diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9883e4..16b0e55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.17.1] - 2023-12-18
+
- add option to enable title's tooltip + change up/down trend arrow colors to grey by [@shahramk](https://github.com/shahramk)
+## [1.17.0] - 2023-12-14
+
+### Added
+
+- DatePicker component by [@amit-y](https://github.com/amit-y)
+
## [1.16.0] - 2023-08-09
### Added
diff --git a/README.md b/README.md
index bf71ba5..0524cbf 100644
--- a/README.md
+++ b/README.md
@@ -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/)
diff --git a/package-lock.json b/package-lock.json
index 06a72be..097ab7b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@newrelic/nr-labs-components",
- "version": "1.16.0",
+ "version": "1.17.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@newrelic/nr-labs-components",
- "version": "1.16.0",
+ "version": "1.17.0",
"license": "Apache-2.0",
"dependencies": {
"dayjs": "^1.11.7"
diff --git a/package.json b/package.json
index 8949af4..3ccea2c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@newrelic/nr-labs-components",
- "version": "1.16.0",
+ "version": "1.17.0",
"description": "New Relic Labs components",
"main": "dist/index.js",
"module": "dist/index.es.js",
diff --git a/src/components/date-picker/README.md b/src/components/date-picker/README.md
new file mode 100644
index 0000000..64d51ae
--- /dev/null
+++ b/src/components/date-picker/README.md
@@ -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 (
+
+
+
+ );
+}
+```
+### Props
+
+- date (date): The default date
+- onChange (function): A function that is called when the user selects a date
diff --git a/src/components/date-picker/index.js b/src/components/date-picker/index.js
new file mode 100644
index 0000000..be014f6
--- /dev/null
+++ b/src/components/date-picker/index.js
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ {formattedMonthYear(new Date(current.yr, current.mo))}
+
+
+
+
+ {DAYS_OF_WEEK.map(({ long, short }) => (
+
+ ))}
+ {Array.from({ length: firstDayOfMonth(current) }, (_, i) => (
+
+ ))}
+ {Array.from({ length: lastDateInMonth(current) }, (_, i) => (
+
clickHandler(i)}
+ >
+ {i + 1}
+
+ ))}
+
+
+
+ );
+};
+
+DatePicker.propTypes = {
+ date: PropTypes.instanceOf(Date),
+ validFrom: PropTypes.instanceOf(Date),
+ onChange: PropTypes.func,
+};
+
+export default DatePicker;
diff --git a/src/components/date-picker/styles.scss b/src/components/date-picker/styles.scss
new file mode 100644
index 0000000..ae4523c
--- /dev/null
+++ b/src/components/date-picker/styles.scss
@@ -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;
+ }
+}
diff --git a/src/components/date-picker/utils.js b/src/components/date-picker/utils.js
new file mode 100644
index 0000000..696ed7b
--- /dev/null
+++ b/src/components/date-picker/utils.js
@@ -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,
+};
diff --git a/src/components/index.js b/src/components/index.js
index 00c1634..312e90f 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -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';