Skip to content

Commit

Permalink
Merge pull request #1250 from cozy/date-month-picker
Browse files Browse the repository at this point in the history
feat: DateMonthPicker
  • Loading branch information
ptbrowne authored Nov 21, 2019
2 parents a12796a + 563eb23 commit bc52948
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 1 deletion.
3 changes: 2 additions & 1 deletion docs/styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ module.exports = {
'../react/SelectBox/SelectBox.jsx',
'../react/Textarea/index.jsx',
'../react/Toggle/index.jsx',
'../react/FileInput/index.jsx'
'../react/FileInput/index.jsx',
'../react/DateMonthPicker/index.jsx'
]
},
{
Expand Down
32 changes: 32 additions & 0 deletions react/DateMonthPicker/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
```
import I18n from 'cozy-ui/transpiled/react/I18n';
import DateMonthPicker from 'cozy-ui/transpiled/react/DateMonthPicker';
import Stack from 'cozy-ui/transpiled/react/Stack';
import Button from 'cozy-ui/transpiled/react/Button';
import Modal from 'cozy-ui/transpiled/react/Modal';
const dictRequire = x => ({})
const initialState = { choosing: isTesting(), monthDate: '2019-08' }
const showPicker = () => setState({ choosing: true });
const hidePicker = () => setState({ choosing: false });
const handleSelect = monthDate => {
setState({ monthDate })
hidePicker()
}
<I18n dictRequire={dictRequire} lang='en'>
<Stack>
Month chosen: { state.monthDate ? state.monthDate : 'No date chosen yet'}<br/>
<Button onClick={showPicker} label='Choose month'/>
{ state.choosing ? <Modal dismissAction={hidePicker}>
<DateMonthPicker
f={x => x}
onSelect={handleSelect}
initialValue={state.monthDate}
/>
<div className='u-mb-2'>{' '}</div>
</Modal>: null }
</Stack>
</I18n>
```
111 changes: 111 additions & 0 deletions react/DateMonthPicker/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import Icon from '../Icon'
import { translate } from '../I18n'
import range from 'lodash/range'
import { format } from 'date-fns'
import styles from './styles.styl'
import cx from 'classnames'

const MonthButton = translate()(({ monthNum, f, onClick, isSelected }) => {
const d = new Date(2019, monthNum, 15)
const handleClick = () => {
onClick(monthNum)
}
return (
<Button
theme="secondary"
className={cx(
styles.DateMonthPicker__MonthButton,
isSelected ? styles.ValueSelected : null
)}
onClick={handleClick}
label={f(d, 'MMM')}
/>
)
})

const useCounter = (initialValue, min, max) => {
const [state, setState] = useState(initialValue)
const increment = () => setState(Math.min(state + 1, max))
const decrement = () => setState(Math.max(state - 1, min))
return [state, decrement, increment]
}

const DateMonthPicker = ({ initialValue, onSelect }) => {
const [initialYear, initialMonth] = initialValue
.split('-')
.map(x => parseInt(x, 10))
const [year, decreaseYear, increaseYear] = useCounter(
parseInt(initialYear, 10),
1990,
Infinity
)

const handleClickMonth = month => {
const d = new Date(year, month, 1)
onSelect(format(d, 'YYYY-MM-DD'))
}

return (
<>
<div className={styles.DateMonthPicker__YearControls}>
<Button
className={styles.DateMonthPicker__YearButton}
theme="secondary"
size="small"
label=""
onClick={decreaseYear}
icon={<Icon icon="left" />}
/>
<div
className={cx(
styles.DateMonthPicker__Year,
year === initialYear && styles.ValueSelected
)}
>
{year}
</div>
<Button
className={styles.DateMonthPicker__YearButton}
theme="secondary"
size="small"
label=""
onClick={increaseYear}
icon={<Icon icon="right" />}
/>
</div>
<div className={styles.DateMonthPicker__MonthGrid}>
{range(0, 12).map(i => (
<MonthButton
key={i}
isSelected={initialMonth - 1 === i && year == initialYear}
monthNum={i}
onClick={handleClickMonth}
/>
))}
</div>
</>
)
}

const dateMonthProp = function(props, propName, componentName) {
if (!/^[0-9]{4}-[0-9]{2}$/.test(props[propName])) {
return new Error(
'Invalid prop `' +
propName +
'` supplied to' +
' `' +
componentName +
'`. Should be in the form YYYY-MM.'
)
}
}

DateMonthPicker.propTypes = {
onSelect: PropTypes.func.isRequired,
initialValue: dateMonthProp
}

export default DateMonthPicker
67 changes: 67 additions & 0 deletions react/DateMonthPicker/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { mount } from 'enzyme'
import React from 'react'
import DateMonthPicker from './index'
import Button from '../Button'
import I18n from '../I18n'
import { act } from 'react-dom/test-utils'
const findButtonWithLabel = (root, label) =>
root.findWhere(n => n.type() == Button && n.props().label === label)

const findButtonWithIcon = (root, iconName) =>
root.findWhere(n => {
const props = n.props()
if (n.type() !== Button) {
return
}
if (!props.icon) {
return
}
return props.icon.props.icon === iconName
})

describe('DateMonthPicker', () => {
const setup = ({ initialValue }) => {
const handleSelect = jest.fn()
const root = mount(
<I18n lang="en" dictRequire={() => {}}>
<DateMonthPicker initialValue={initialValue} onSelect={handleSelect} />
</I18n>
)
return { root, handleSelect }
}

it('should be able to select a month', async () => {
const { root, handleSelect } = setup({ initialValue: '2015-08' })
const februaryButton = findButtonWithLabel(root, 'Feb')
expect(februaryButton.length).toBe(1)
februaryButton.props().onClick()
expect(handleSelect).toHaveBeenCalledTimes(1)
expect(handleSelect).toHaveBeenCalledWith('2015-02-01')
})

it('should be able to go to previous year', async () => {
const { root, handleSelect } = setup({ initialValue: '2015-08' })
const prevYearButton = findButtonWithIcon(root, 'left')
act(() => {
prevYearButton.props().onClick()
})
root.update()
const februaryButton = findButtonWithLabel(root, 'Feb')
februaryButton.props().onClick()
expect(handleSelect).toHaveBeenCalledTimes(1)
expect(handleSelect).toHaveBeenCalledWith('2014-02-01')
})

it('should be able to go to next year', async () => {
const { root, handleSelect } = setup({ initialValue: '2015-08' })
const nextYearButton = findButtonWithIcon(root, 'right')
act(() => {
nextYearButton.props().onClick()
})
root.update()
const februaryButton = findButtonWithLabel(root, 'Feb')
februaryButton.props().onClick()
expect(handleSelect).toHaveBeenCalledTimes(1)
expect(handleSelect).toHaveBeenCalledWith('2016-02-01')
})
})
37 changes: 37 additions & 0 deletions react/DateMonthPicker/styles.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.DateMonthPicker__YearControls
justify-content center
display flex
align-items center

.DateMonthPicker__YearButton
flex-grow 0
min-width 3rem
min-height 3rem
text-align center
border-width 0
margin 0
color var(--coolGrey)
background none !important

.DateMonthPicker__Year
display inline-flex

.DateMonthPicker__MonthGrid
display grid
grid-template-columns repeat(4, auto)
grid-template-rows repeat(3, 1fr)
overflow hidden


.DateMonthPicker__MonthButton
min-height 3rem
min-width 0
margin 0
border 0
border-radius 0
font-weight normal
text-transform none

.ValueSelected
color var(--primaryColor)
font-weight bold

0 comments on commit bc52948

Please sign in to comment.