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

Sortable Resource List #1098

Merged
merged 24 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
828b062
Sortable Resource List
francocorreasosa Nov 19, 2018
cd73dea
Finish implementing sortable resource list
francocorreasosa Nov 23, 2018
c4b5ec4
Fixes
francocorreasosa Nov 23, 2018
64ee6ef
Merge branch 'master' into sortable-resource-list
Nov 23, 2018
920023f
Add reorder drag handle
francocorreasosa Dec 3, 2018
81b551c
Drag handle
francocorreasosa Dec 4, 2018
0c07ab8
Backmerge from master
francocorreasosa Dec 4, 2018
5ec2e6a
Adjust drag handle and style dragging state resourcelist item
francocorreasosa Dec 4, 2018
c2ffea9
Fix background color on dragging
francocorreasosa Dec 4, 2018
fcaf754
Merge branch 'master' into sortable-resource-list
Dec 4, 2018
569f0b4
Accessibility WIP
francocorreasosa Dec 7, 2018
f01c3f4
Merge branch 'sortable-resource-list' of github.com:auth0/cosmos into…
francocorreasosa Dec 7, 2018
30bde0b
Backmerge master
francocorreasosa Dec 9, 2018
35201ea
Enhance event handling
francocorreasosa Dec 10, 2018
8b68292
Backmerge from master
francocorreasosa Dec 17, 2018
81c470d
Remove extra accessibility code
francocorreasosa Dec 17, 2018
0911372
Fix broken story
francocorreasosa Dec 17, 2018
391eb26
Fix stories
francocorreasosa Dec 17, 2018
c518755
Backmerge from master
francocorreasosa Dec 18, 2018
df2592e
Remove tabs console.log
francocorreasosa Dec 18, 2018
41e130f
Merge branch 'master' of github.com:auth0/cosmos
francocorreasosa Dec 18, 2018
67523db
Merge branch 'master' of github.com:auth0/cosmos
francocorreasosa Dec 18, 2018
da8b348
Backmerge from master
francocorreasosa Dec 18, 2018
4e59451
Remove pointer events on non-dragging elements
francocorreasosa Dec 18, 2018
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
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