Skip to content
This repository has been archived by the owner on May 17, 2023. It is now read-only.

Commit

Permalink
Sortable Resource List (#1098)
Browse files Browse the repository at this point in the history
Now, the `sortable` prop can be used to make the list sortable by dragging and dropping items. You will need to implement the `onSortEnd` method in order to reorder the items using the provided `arrayMove`

![kapture 2018-12-04 at 18 22 31](https://user-images.githubusercontent.com/4152942/49473729-9bed4f80-f7f1-11e8-87c3-5638d8d95bac.gif)
  • Loading branch information
Franco Correa authored and landitus committed Dec 18, 2018
1 parent 2ebe530 commit c7c486c
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 84 deletions.
3 changes: 2 additions & 1 deletion core/components/molecules/resource-list/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ResourceList from './resource-list'
import ResourceList, { arrayMove } from './resource-list'

export default ResourceList
export { arrayMove }
134 changes: 90 additions & 44 deletions core/components/molecules/resource-list/item/item.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import styled, { css } from '@auth0/cosmos/styled'
import { Button, ButtonGroup, Link } from '@auth0/cosmos'
import Avatar, { StyledAvatar } from '@auth0/cosmos/atoms/avatar'
import { StyledTextAllCaps } from '@auth0/cosmos/atoms/text'
Expand All @@ -9,7 +9,8 @@ import { __ICONNAMES__ } from '@auth0/cosmos/atoms/icon'
import { colors, spacing } from '@auth0/cosmos-tokens'
import Automation from '../../../_helpers/automation-attribute'
import { actionToButtonProps, buttonBuilder } from '../action-builder'
import { deprecate } from '../../../_helpers/custom-validations'

const itemFocusOutline = '2px'

/**
* Builds the button from the action or
Expand Down Expand Up @@ -37,54 +38,79 @@ const resolveAction = (item, action, key) => {
*/
const resolveActions = (actions, item) => actions.map(resolveAction.bind(this, item))

const ListItem = props => {
let image
let title
let subtitle
let actions
class ListItem extends React.Component {
constructor(props) {
super(props)
this.dragHandler = React.createRef()
}

renderSortingHandler() {
if (this.props.reorderHandle) {
const SortableHandler = this.props.reorderHandle

const callHandler = handler => evt => handler(evt, props.item)
return <SortableHandler ref={this.dragHandler} />
}

if (props.image) {
// TODO: We might want a way to control the type of the avatar, but we don't
// want to leak every prop from Avatar into the ListItem...
image = <Avatar type="resource" image={props.image} size="large" />
} else if (props.icon) {
image = <Avatar type="resource" icon={props.icon} size="large" />
return null
}

if (props.title) {
if (props.href) {
title = <Link href={props.href}>{props.title}</Link>
} else {
title = props.title
renderImage() {
if (this.props.image) {
// TODO: We might want a way to control the type of the avatar, but we don't
// want to leak every prop from Avatar into the ListItem...
return <Avatar type="resource" image={this.props.image} size="large" />
} else if (this.props.icon) {
return <Avatar type="resource" icon={this.props.icon} size="large" />
}

return null
}

if (props.subtitle) {
subtitle = <ListItem.Subtitle>{props.subtitle}</ListItem.Subtitle>
renderTitle() {
if (this.props.title) {
if (this.props.href) {
return <Link href={this.props.href}>{this.props.title}</Link>
} else {
return this.props.title
}
}

return null
}

if (props.actions) {
actions = <ButtonGroup>{resolveActions(props.actions, props.item)}</ButtonGroup>
renderSubtitle() {
return this.props.subtitle ? <ListItem.Subtitle>{this.props.subtitle}</ListItem.Subtitle> : null
}

return (
<ListItem.Element
onClick={props.onClick ? callHandler(props.onClick) : null}
{...Automation('resource-list.item')}
>
<ListItem.Header>
{image}
<div>
{title}
{subtitle}
</div>
</ListItem.Header>
{props.children && <ListItem.Body>{props.children}</ListItem.Body>}
{props.actions && <ListItem.Footer>{actions}</ListItem.Footer>}
</ListItem.Element>
)
renderActions() {
return this.props.actions ? (
<ButtonGroup align="right">{resolveActions(this.props.actions, this.props.item)}</ButtonGroup>
) : null
}

render() {
const props = this.props
const callHandler = handler => evt => handler(evt, props.item)

return (
<ListItem.Element
draggingMode={props.draggingMode}
onClick={props.onClick ? callHandler(props.onClick) : null}
{...Automation('resource-list.item')}
>
{this.renderSortingHandler()}
<ListItem.Header>
{this.renderImage()}
<div>
{this.renderTitle()}
{this.renderSubtitle()}
</div>
</ListItem.Header>
{props.children && <ListItem.Body>{props.children}</ListItem.Body>}
{props.actions && <ListItem.Footer>{this.renderActions()}</ListItem.Footer>}
</ListItem.Element>
)
}
}

ListItem.Element = styled.li`
Expand All @@ -95,12 +121,31 @@ ListItem.Element = styled.li`
border-top: 1px solid ${colors.list.borderColor};
padding: ${spacing.small} ${spacing.xsmall};
cursor: ${props => (props.onClick ? 'pointer' : 'inherit')};
&:hover {
background: ${colors.list.backgroundHover};
}
&.cosmos-dragging {
background-color: ${colors.base.white};
opacity: 0.9;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.2);
}
> *:not(:last-child) {
margin-right: ${spacing.small};
}
/* Disable pointer events on non-dragging elements */
/* to avoid unexpected hover behaviors. */
${props =>
props.draggingMode
? css`
&:not(.cosmos-dragging) {
pointer-events: none;
}
`
: ''};
`

ListItem.Header = styled.div`
Expand Down Expand Up @@ -158,11 +203,12 @@ ListItem.propTypes = {
const firstAction = props.actions[0]

if (!React.isValidElement(firstAction)) {
return deprecate(props, {
name: 'actions',
oldAPI: 'passing objects in actions',
replacement: '<Button>'
})
// See: https://github.com/auth0/cosmos/issues/1133
// See: https://github.com/auth0/cosmos/issues/1222
console.warn(
'Passing objects in actions is deprecated and will be removed in Cosmos 1.0.' +
' See https://github.com/auth0/cosmos/pull/1133 for more information.'
)
}
}
}
Expand Down
124 changes: 109 additions & 15 deletions core/components/molecules/resource-list/resource-list.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,118 @@
import React from 'react'
import PropTypes from 'prop-types'
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc'
import styled from '@auth0/cosmos/styled'
import ResourceListItem from './item'
import { spacing } from '@auth0/cosmos-tokens'
import { actionShapeWithRequiredIcon } from '@auth0/cosmos/_helpers/action-shape'
import Automation from '../../_helpers/automation-attribute'
import containerStyles from '../../_helpers/container-styles'
import SortableListHandle from './sortable-handle'

const defaultItemRenderer = item => <ResourceListItem {...item} />

const ResourceList = props => (
<ResourceList.Element {...Automation('resource-list')}>
{props.items.map((item, index) => {
const itemRenderer = props.renderItem || defaultItemRenderer
return React.cloneElement(itemRenderer(item, index), {
key: item.key || index,
actions: item.actions || props.actions,
onClick: item.onClick || props.onItemClick,
item
const ResourceList = props => {
const defaultItemRenderer = (item, _, index) => {
// We can say we are in dragging mode if there one .cosmos-dragging
// element in the DOM.
const draggingMode = document.querySelector('.cosmos-dragging')
return (
<ResourceListItem
index={index}
draggingMode={draggingMode}
reorderHandle={props.sortable ? SortableListHandle : null}
{...item}
/>
)
}

const itemRendererBuilder = props => {
const {
item,
index,
actions,
renderItem,
onItemClick,
accessibilityIndex,
accessibilityOnSortEnd
} = props
const itemRenderer = renderItem || defaultItemRenderer
const actualIndex = index || accessibilityIndex

return React.cloneElement(itemRenderer(item, props, actualIndex), {
item,
key: actualIndex,
index: actualIndex,
accessibilityOnSortEnd,
actions: item.actions || actions,
onClick: item.onClick || onItemClick
})
}

const defaultChildrenRenderer = ({ items, actions, onItemClick, renderItem }) =>
items.map((item, index) =>
itemRendererBuilder({ item, index, renderItem, onItemClick, actions })
)

const SortableResourceListItem = SortableElement(
({
item,
actions,
renderItem,
accessibilityIndex,
onClick: onItemClick,
accessibilityOnSortEnd
}) =>
itemRendererBuilder({
item,
actions,
renderItem,
accessibilityIndex,
onItemClick,
accessibilityOnSortEnd
})
})}
</ResourceList.Element>
)
)

const SortableResourceList = SortableContainer(
({ items: sortableItems, actions, onItemClick, renderItem, accessibilityOnSortEnd }) => (
<div>
{sortableItems.map((item, index) => (
<SortableResourceListItem
actions={item.actions || actions}
onClick={item.onClick || onItemClick}
item={item}
key={index}
// Need to pass accessibilityIndex due to index being omitted
// when calling child component.
// See: https://github.com/clauderic/react-sortable-hoc/blob/0077f0b4e3b50f68c04672e78b6b69b8dc880d96/src/SortableElement/index.js#L89
accessibilityIndex={index}
accessibilityOnSortEnd={accessibilityOnSortEnd}
index={index}
renderItem={renderItem}
/>
))}
</div>
)
)

const sortableChildrenRenderer = props => {
return (
<SortableResourceList
{...props}
useDragHandle={true}
helperClass="cosmos-dragging"
accessibilityOnSortEnd={props.onSortEnd}
/>
)
}

const resolveChildrenRenderer = props =>
props.sortable ? sortableChildrenRenderer(props) : defaultChildrenRenderer(props)

return (
<ResourceList.Element {...Automation('resource-list')}>
{resolveChildrenRenderer(props)}
</ResourceList.Element>
)
}

ResourceList.Element = styled.ul`
${containerStyles};
Expand All @@ -43,7 +134,10 @@ ResourceList.propTypes = {
/** A function that will be called when an item is clicked. */
onItemClick: PropTypes.func,
/** A function that accepts an item from the items array, and returns a ResourceList.Item. */
renderItem: PropTypes.func
renderItem: PropTypes.func,
/** Whether the resource list will be sortable by the user or not */
sortable: PropTypes.bool
}

export default ResourceList
export { arrayMove }
40 changes: 40 additions & 0 deletions core/components/molecules/resource-list/resource-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,43 @@ If you need more control over the rendering of list items, you can pass a `rende
]}
/>
```

## Advanced: Drag and sort items

You can use the `sortable` prop to make the list sortable by dragging and dropping the items.
You will need to implement the `onSortEnd` method in order to reorder the items using the provided
`arrayMove` function

```js
class SortableResourceListExample extends React.Component {
constructor(props) {
super(props)
this.state = {
items: [
{ title: 'Title One', subtitle: 'Subtitle One', href: 'https://auth0.com/' },
{ title: 'Title Two', subtitle: 'Subtitle Two', href: 'https://auth0.com/' },
{ title: 'Title Three', subtitle: 'Subtitle Three', href: 'https://auth0.com/' }
]
}
}
onSortEnd({ oldIndex, newIndex }) {
// The arrayMove example does not work on docs
this.setState({
items: arrayMove(this.state.items, oldIndex, newIndex)
})
}
render() {
return (
<ResourceList
actions={[
{ icon: 'settings', handler: function() {}, label: 'Settings' },
{ icon: 'delete', handler: function() {}, label: 'Delete' }
]}
sortable
items={this.state.items}
onSortEnd={event => this.onSortEnd(event)}
/>
)
}
}
```
Loading

0 comments on commit c7c486c

Please sign in to comment.