Skip to content
This repository has been archived by the owner on Oct 8, 2022. It is now read-only.

Commit

Permalink
Allow to rename lanes (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
lourenci authored Jul 28, 2019
1 parent 15f9b21 commit 7a3fc71
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 42 deletions.
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Yet another Kanban/Trello board lib for React.

* 👊 Reliable: 100% tested on CI; 100% coverage; 100% SemVer.
* 🎮 Having fun: Play with Hooks 🎣 and Styled Components 💅🏻.
* ♿️ Acessible: Keyboard and mobile friendly.
* ♿️ Accessible: Keyboard and mobile friendly.

## 🛠 Install and usage

Expand Down Expand Up @@ -73,12 +73,14 @@ const board = {
| `onLaneDragEnd` | Callback that will be called when the lane move ends |
| `renderCard` | A card to be rendered instead of the default card |
| `renderLaneHeader` | A lane header to be rendered instead of the default lane header |
| `allowAddLane` (required if `onNewLane`) | Allow a new lane be added by the user |
| `allowAddLane` | Allow a new lane be added by the user |
| `onNewLane` (required if `allowAddLane`) | Callback that will be called when a new lane is added |
| `disableLaneDrag` | Disable the lane move |
| `disableCardDrag` | Disable the card move |
| `allowRemoveLane` | Allow to remove a lane in default lane header |
| `onLaneRemove` | Callback that will be called when a lane is removed |
| `onLaneRemove` (required if `allowRemoveLane` | Callback that will be called when a lane is removed |
| `allowRenameLane` | Allow to rename a lane in default lane header |
| `onLaneRename` (required if `allowRenameLane` | Callback that will be called when a lane is renamed |

#### `children`
```js
Expand Down Expand Up @@ -169,7 +171,13 @@ The function will receive these parameters:
| Arg | Description |
|--------------|------------------------------------------------------- |
| `lane` | The lane props |
| `removeLane` | A function to remove the lane |
| `laneBag` | A bag with some helper functions to work with the lane |

##### `laneBag`
| function | Description |
|--------------|------------------------------------------------------- |
| `removeLane` | Call this function to remove the lane from the board |
| `renameLane` | Call this function with a title to rename the lane |

Ex.:
```js
Expand All @@ -187,9 +195,11 @@ const board = {
}
<Board
renderLaneHeader={({ title }, removeLane) => (
renderLaneHeader={({ title }, { removeLane, renameLane }) => (
<YourLaneHeader>
{title} - <button type='button' onClick={removeLane}>Remove Lane</button>
{title}
<button type='button' onClick={removeLane}>Remove Lane</button>
<button type='button' onClick={() => renameLane('New title')}>Rename Lane</button>
</YourLaneHeader
)}
>
Expand All @@ -198,7 +208,7 @@ const board = {
```
#### `allowAddLane`
Allow the user to add a new lane directly by the board. Use this together with `onNewLane` prop.
Allow the user to add a new lane directly by the board.
#### `onNewLane`
When the user adds a new lane, this callback will be called passing the lane title typed by the user.
Expand All @@ -221,6 +231,9 @@ Disallow the user from move a lane.
#### `disableCardDrag`
Disallow the user from move a card.
### `AllowRemoveLane`
When using the default header template, when you don't pass a template through the `renderLaneHeader`, it will allow the user to remove a lane.
#### `onLaneRemove`
When the user removes a lane, this callback will be called passing these parameters:
Expand All @@ -229,6 +242,17 @@ When the user removes a lane, this callback will be called passing these paramet
| `board` | The board without the removed lane |
| `lane` | The removed lane |
### `AllowRenameLane`
When using the default header template, when you don't pass a template through the `renderLaneHeader`, it will allow the user to rename a lane.
#### `onLaneRename`
When the user renames a lane, this callback will be called passing these parameters:
| Arg | Description |
|--------------|------------------------------------------------------- |
| `board` | The board with the renamed lane |
| `lane` | The renamed lane |
## 🚴‍♀️ Roadmap
You can view the next features [here](https://github.com/lourenci/react-kanban/milestone/1).
Expand Down
11 changes: 10 additions & 1 deletion assets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,13 @@ const board = {
]
}

render(<Board {...getUrlParams()}>{board}</Board>, document.getElementById('app'))
render(
<Board
{...getUrlParams()}
onLaneRemove={console.log}
onLaneRename={console.log}
>
{board}
</Board>,
document.getElementById('app')
)
83 changes: 83 additions & 0 deletions src/components/Board/components/DefaultLaneHeader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { Fragment, useState } from 'react'
import styled from 'styled-components'

const LaneHeaderSkeleton = styled.div`
padding-bottom: 10px;
font-weight: bold;
display: flex;
justify-content: space-between;
span:nth-child(2) {
cursor: pointer;
}
`

const CursorPointer = styled.span`
cursor: pointer;
`

const DefaultButton = styled.button`
color: #333333;
background-color: #FFFFFF;
border-color: #CCCCCC;
:hover, :focus, :active {
background-color: #E6E6E6;
}
`

const Input = styled.input`
:focus {
outline: none;
}
`

function useRenameMode (state) {
const [renameMode, setRenameMode] = useState(state)

function toggleRenameMode () {
setRenameMode(!renameMode)
}

return [renameMode, toggleRenameMode]
}

export default function ({ children: lane, allowRemoveLane, onLaneRemove, allowRenameLane, onLaneRename }) {
const [renameMode, toggleRenameMode] = useRenameMode(false)
const [title, setTitle] = useState(lane.title)
const [titleInput, setTitleInput] = useState('')

function handleRenameLane (title) {
onLaneRename(lane.id, title)
setTitle(title)
toggleRenameMode()
}

function handleRenameMode () {
setTitleInput(title)
toggleRenameMode()
}

return (
<LaneHeaderSkeleton>
{allowRenameLane && renameMode ? (
<Fragment>
<span>
<Input type='text' value={titleInput} onChange={({ target: { value } }) => setTitleInput(value)} autoFocus />
</span>
<span>
<DefaultButton type='button' onClick={() => handleRenameLane(titleInput)}>Rename</DefaultButton>
<DefaultButton type='button' onClick={handleRenameMode}>Cancel</DefaultButton>
</span>
</Fragment>
) : (
<Fragment>
<CursorPointer onClick={handleRenameMode}>
{title}
</CursorPointer>
{allowRemoveLane && <span onClick={() => onLaneRemove(lane)}>×</span>}
</Fragment>
)}
</LaneHeaderSkeleton>
)
}
148 changes: 148 additions & 0 deletions src/components/Board/components/DefaultLaneHeader/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import DefaultLaneHeader from './'

describe('<DefaultLaneHeader />', () => {
let subject

const onLaneRemove = jest.fn()
const onLaneRename = jest.fn()

const lane = { id: 1, title: 'Lane title' }

function reset () {
subject = undefined
onLaneRemove.mockClear()
onLaneRename.mockClear()
}

function mount (props) {
subject = render(
<DefaultLaneHeader onLaneRemove={onLaneRemove} onLaneRename={onLaneRename} {...props}>
{lane}
</DefaultLaneHeader>
)
return subject
}

beforeEach(reset)

it('renders a lane header with the title', () => {
expect(mount().queryByText('Lane title')).toBeInTheDocument()
})

describe('about the remove lane button', () => {
describe('when the component does not receive the allowRemoveLane prop', () => {
beforeEach(() => mount({ onLaneRemove }))

it('does not show the remove button', () => {
expect(subject.queryByText('×')).not.toBeInTheDocument()
})

it('does not call the "onLaneRemove" callback', () => {
expect(onLaneRemove).not.toHaveBeenCalled()
})
})

describe('when the component receives the "allowRemoveLane" prop', () => {
beforeEach(() => mount({ allowRemoveLane: true }))

it('shows the remove button', () => {
expect(subject.queryByText('×')).toBeInTheDocument()
})

it('does not call the "onLaneRemove" callback', () => {
expect(onLaneRemove).not.toHaveBeenCalled()
})

describe('when the user clicks on the remove button', () => {
beforeEach(() => fireEvent.click(subject.queryByText('×')))

it('calls the "onLaneRemove" callback passing the lane', () => {
expect(onLaneRemove).toHaveBeenCalledTimes(1)
expect(onLaneRemove).toHaveBeenCalledWith(lane)
})
})
})
})

describe('about the lane title renaming', () => {
describe('when the component does not receive the "allowRenameLane" prop', () => {
describe('when the user clicks on the lane title', () => {
beforeEach(() => {
mount({ onLaneRename })
fireEvent.click(subject.queryByText('Lane title'))
})

it('does not allow the user to rename the lane', () => {
expect(subject.queryByText('Lane title')).toBeInTheDocument()
expect(subject.container.querySelector('input')).not.toBeInTheDocument()
})

it('does not call the "onLaneRename" callback', () => {
expect(onLaneRename).not.toHaveBeenCalled()
})
})
})

describe('when the component receives the "allowRenameLane" prop', () => {
describe('when the user clicks on the lane title', () => {
beforeEach(() => {
mount({ allowRenameLane: true, onLaneRename })
fireEvent.click(subject.queryByText('Lane title'))
})

it('does not call the "onLaneRename" callback', () => {
expect(onLaneRename).not.toHaveBeenCalled()
})

it('toggles the title for an input for typing a new title', () => {
expect(subject.queryByText('Lane title')).not.toBeInTheDocument()
expect(subject.container.querySelector('input')).toBeInTheDocument()
expect(subject.queryByText('Rename', { selector: 'button' })).toBeInTheDocument()
expect(subject.queryByText('Cancel', { selector: 'button' })).toBeInTheDocument()
})

it('focuses on the input', () => {
expect(subject.container.querySelector('input')).toHaveFocus()
})

it('fills the input with the lane title', () => {
expect(subject.container.querySelector('input')).toHaveValue('Lane title')
})

describe('when the user types a new name and confirms it', () => {
beforeEach(() => {
fireEvent.change(subject.container.querySelector('input'), { target: { value: 'New title' } })
fireEvent.click(subject.queryByText('Rename', { selector: 'button' }))
})

it('toggles the input for the new lane title', () => {
expect(subject.queryByText('New title')).toBeInTheDocument()
expect(subject.container.querySelector('input')).not.toBeInTheDocument()
})

it('calls the "onLaneRename" callback passing the lane id with the new title', () => {
expect(onLaneRename).toHaveBeenCalledTimes(1)
expect(onLaneRename).toHaveBeenCalledWith(1, 'New title')
})
})

describe('when the user cancels the renaming', () => {
beforeEach(() => {
fireEvent.click(subject.queryByText('Cancel', { selector: 'button' }))
})

it('cancels the renaming', () => {
expect(subject.queryByText('Lane title')).toBeInTheDocument()
expect(subject.container.querySelector('input')).not.toBeInTheDocument()
})

it('does call the "onLaneRename" callback', () => {
expect(onLaneRename).not.toHaveBeenCalled()
})
})
})
})
})
})
Loading

0 comments on commit 7a3fc71

Please sign in to comment.