Skip to content

Commit

Permalink
Merge pull request #79 from newrelic/date-picker
Browse files Browse the repository at this point in the history
feat: date picker component
  • Loading branch information
amit-y authored Dec 14, 2023
2 parents bb7f197 + 980329d commit 3b6ff32
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- DatePicker component by [@amit-y](https://github.com/amit-y)

## [1.16.0] - 2023-08-09

### Added
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ A component that renders a progress bar.
### [FilterBar](src/components/filter-bar)
Component that allows a user to filter options.

### [DatePicker](src/components/date-picker)
Component that displays a calendar to select a date

## Utilities

### [timeRangeToNrql](src/utils/time-range-to-nrql/)
Expand Down
26 changes: 26 additions & 0 deletions src/components/date-picker/README.md
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
121 changes: 121 additions & 0 deletions src/components/date-picker/index.js
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;
59 changes: 59 additions & 0 deletions src/components/date-picker/styles.scss
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;
}
}
87 changes: 87 additions & 0 deletions src/components/date-picker/utils.js
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,
};
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { default as StatusIcon } from './status-icon';
export { default as StatusIconsLayout } from './status-icons-layout';
export { default as ProgressBar } from './progress-bar';
export { default as FilterBar } from './filter-bar';
export { default as DatePicker } from './date-picker';

0 comments on commit 3b6ff32

Please sign in to comment.