-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #79 from newrelic/date-picker
feat: date picker component
- Loading branch information
Showing
7 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# DatePicker | ||
|
||
The `DatePicker` component displays a calendar to select a date. | ||
|
||
## Usage | ||
|
||
To use the component, simply import and use: | ||
|
||
```jsx | ||
import React, { useState } from 'react'; | ||
import { DatePicker } from '@newrelic/nr-labs-components'; | ||
|
||
function MyComponent() { | ||
const [selectedDate, setSelectedDate] = useState(new Date()); | ||
|
||
return ( | ||
<div> | ||
<DatePicker date={selectedDate} onChange={setSelectedDate} /> | ||
</div> | ||
); | ||
} | ||
``` | ||
### Props | ||
|
||
- date (date): The default date | ||
- onChange (function): A function that is called when the user selects a date |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import React, { useEffect, useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import { Icon, Popover, PopoverTrigger, PopoverBody, TextField } from 'nr1'; | ||
|
||
import { | ||
formattedDateField, | ||
formattedMonthYear, | ||
firstDayOfMonth, | ||
lastDateInMonth, | ||
extractDateParts, | ||
selectedDate, | ||
daysOfWeek, | ||
isSelectableDate, | ||
} from './utils'; | ||
|
||
const DAYS_OF_WEEK = daysOfWeek(); | ||
|
||
import styles from './styles.scss'; | ||
|
||
const DatePicker = ({ date, onChange, validFrom }) => { | ||
const [opened, setOpened] = useState(false); | ||
const [current, setCurrent] = useState(extractDateParts(new Date())); | ||
|
||
useEffect(() => { | ||
if (!date || !(date instanceof Date)) return; | ||
setCurrent(extractDateParts(date)); | ||
}, [date]); | ||
|
||
const prevMonth = () => { | ||
const prevMo = new Date(current.yr, current.mo - 1); | ||
setCurrent(extractDateParts(prevMo)); | ||
}; | ||
|
||
const nextMonth = () => { | ||
const nextMo = new Date(current.yr, current.mo + 1); | ||
setCurrent(extractDateParts(nextMo)); | ||
}; | ||
|
||
const isDateInCurrentMonth = (d = new Date()) => | ||
d.getFullYear() === current.yr && d.getMonth() === current.mo; | ||
|
||
const clickHandler = (dt) => { | ||
if (!isSelectableDate(current, dt + 1, validFrom) || !onChange) return; | ||
|
||
const d = date instanceof Date ? new Date(date.getTime()) : new Date(); | ||
d.setFullYear(current.yr); | ||
d.setMonth(current.mo); | ||
d.setDate(dt + 1); | ||
onChange(d); | ||
|
||
setOpened(false); | ||
}; | ||
|
||
const changeHandler = (_, o) => setOpened(o); | ||
|
||
return ( | ||
<Popover opened={opened} onChange={changeHandler}> | ||
<PopoverTrigger> | ||
<TextField | ||
className="date-picker-text-field" | ||
value={formattedDateField(date)} | ||
placeholder="Select a date" | ||
readOnly | ||
/> | ||
</PopoverTrigger> | ||
<PopoverBody> | ||
<div className={`${styles.calendar}`}> | ||
<div className={`${styles.cell} ${styles.prev}`} onClick={prevMonth}> | ||
<Icon type={Icon.TYPE.INTERFACE__CHEVRON__CHEVRON_LEFT} /> | ||
</div> | ||
<div className={`${styles.cell} ${styles['mo-yr']}`}> | ||
{formattedMonthYear(new Date(current.yr, current.mo))} | ||
</div> | ||
<div | ||
className={`${styles.cell} ${ | ||
isDateInCurrentMonth() ? styles.disabled : styles.next | ||
}`} | ||
onClick={isDateInCurrentMonth() ? null : nextMonth} | ||
> | ||
<Icon type={Icon.TYPE.INTERFACE__CHEVRON__CHEVRON_RIGHT} /> | ||
</div> | ||
{DAYS_OF_WEEK.map(({ long, short }) => ( | ||
<div className={`${styles.cell} ${styles.day}`} key={long}> | ||
<abbr title={long}>{short}</abbr> | ||
</div> | ||
))} | ||
{Array.from({ length: firstDayOfMonth(current) }, (_, i) => ( | ||
<div | ||
key={`${current.yr}${current.mo}blank${i}`} | ||
className={`${styles.cell}`} | ||
/> | ||
))} | ||
{Array.from({ length: lastDateInMonth(current) }, (_, i) => ( | ||
<div | ||
key={`${current.yr}${current.mo}${i}`} | ||
className={`${styles.cell} ${styles.date} ${ | ||
selectedDate(i, current, date) ? styles.selected : '' | ||
} ${ | ||
!isSelectableDate(current, i + 1, validFrom) | ||
? styles.disabled | ||
: '' | ||
}`} | ||
onClick={() => clickHandler(i)} | ||
> | ||
{i + 1} | ||
</div> | ||
))} | ||
</div> | ||
</PopoverBody> | ||
</Popover> | ||
); | ||
}; | ||
|
||
DatePicker.propTypes = { | ||
date: PropTypes.instanceOf(Date), | ||
validFrom: PropTypes.instanceOf(Date), | ||
onChange: PropTypes.func, | ||
}; | ||
|
||
export default DatePicker; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
.calendar { | ||
padding: 20px; | ||
display: grid; | ||
grid-template-columns: repeat(7, 1fr); | ||
} | ||
|
||
.cell { | ||
display: inline-block; | ||
width: 32px; | ||
height: 30px; | ||
line-height: 30px; | ||
text-align: center; | ||
font-size: 12px; | ||
font-weight: 600; | ||
color: #293338; | ||
user-select: none; | ||
|
||
&.day { | ||
font-size: 11px; | ||
} | ||
|
||
&.mo-yr { | ||
font-size: 14px; | ||
grid-column: 2 / 7; | ||
white-space: nowrap; | ||
justify-self: center; | ||
width: 100%; | ||
} | ||
|
||
&.prev, | ||
&.next, | ||
&.date { | ||
cursor: pointer; | ||
} | ||
|
||
&.selected { | ||
border-radius: 3px; | ||
background-color: #293338; | ||
color: #fafbfb; | ||
} | ||
|
||
&.disabled { | ||
color: #9ea5a9; | ||
font-weight: 400; | ||
cursor: auto; | ||
} | ||
|
||
&.date { | ||
&:not(.selected):not(.disabled) { | ||
&:hover { | ||
color: #0c74df; | ||
} | ||
} | ||
} | ||
|
||
abbr { | ||
text-decoration: none; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
const dateFieldFormatter = new Intl.DateTimeFormat('default', { | ||
year: 'numeric', | ||
month: 'short', | ||
day: 'numeric', | ||
}); | ||
|
||
const formattedDateField = (dt) => | ||
dt && dt instanceof Date ? dateFieldFormatter.format(dt) : ''; | ||
|
||
const monthYearFormatter = new Intl.DateTimeFormat('default', { | ||
year: 'numeric', | ||
month: 'long', | ||
}); | ||
|
||
const formattedMonthYear = (dt) => | ||
dt && dt instanceof Date ? monthYearFormatter.format(dt) : ''; | ||
|
||
const firstDayOfMonth = (d) => new Date(d.yr, d.mo).getDay(); | ||
|
||
const lastDateInMonth = (d) => new Date(d.yr, d.mo + 1, 0).getDate(); | ||
|
||
const extractDateParts = (d) => ({ | ||
yr: d.getFullYear(), | ||
mo: d.getMonth(), | ||
dt: d.getDate(), | ||
}); | ||
|
||
const afterToday = (cur, d) => { | ||
const today = new Date(); | ||
return ( | ||
cur.yr === today.getFullYear() && | ||
cur.mo === today.getMonth() && | ||
d > today.getDate() | ||
); | ||
}; | ||
|
||
const isSelectableDate = (cur, d, validFrom) => { | ||
let isValid = true; | ||
if (validFrom && validFrom instanceof Date) { | ||
const validDate = new Date( | ||
validFrom.getFullYear(), | ||
validFrom.getMonth(), | ||
validFrom.getDate() | ||
); | ||
const curDt = new Date(cur.yr, cur.mo, d); | ||
isValid = curDt >= validDate; | ||
} | ||
return isValid && !afterToday(cur, d); | ||
}; | ||
|
||
const selectedDate = (index, cur, dt) => { | ||
if (!dt || !(dt instanceof Date)) return false; | ||
return ( | ||
dt.getFullYear() === cur.yr && | ||
dt.getMonth() === cur.mo && | ||
dt.getDate() === index + 1 | ||
); | ||
}; | ||
|
||
const daysOfWeek = () => { | ||
const now = Date.now(); | ||
const millisecondsInDay = 24 * 60 * 60 * 1000; | ||
const startDayInMs = now - new Date().getDay() * millisecondsInDay; | ||
const formats = ['long', 'short']; | ||
const formatters = formats.map( | ||
(fmt) => new Intl.DateTimeFormat('default', { weekday: fmt }) | ||
); | ||
|
||
return Array.from({ length: 7 }).map((_, i) => { | ||
const d = new Date(startDayInMs + i * millisecondsInDay); | ||
return formats.reduce( | ||
(acc, fmt, idx) => ({ ...acc, [fmt]: formatters[idx].format(d) }), | ||
{} | ||
); | ||
}); | ||
}; | ||
|
||
export { | ||
formattedDateField, | ||
formattedMonthYear, | ||
firstDayOfMonth, | ||
lastDateInMonth, | ||
extractDateParts, | ||
selectedDate, | ||
daysOfWeek, | ||
isSelectableDate, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters