feat: ui v2 - Date Range Component (#1537)

* feat: Date Range wrapper component

* fix yarn issues

* add calendar icon

* date range css styles

* date range fixes

* fix propTypes warning

* fix clear method, defaultProps & onChange method

* update inline styles to use tailwind classes

* date range documentation page

* minor refactor
This commit is contained in:
Rodrigo Antinarelli 2020-03-19 19:58:13 -03:00 committed by James A. Petts
parent 0ec277dd56
commit b183e78f47
13 changed files with 407 additions and 38 deletions

View File

@ -3,6 +3,7 @@ import * as utils from './src/utils/';
export {
Button,
ButtonGroup,
DateRange,
Icon,
IconButton,
Svg,

View File

@ -28,17 +28,19 @@
"classnames": "^2.2.6",
"date-fns": "^2.10.0",
"docz": "^2.2.0",
"theme-ui": "^0.2.38",
"gatsby": "2.19.24",
"gatsby-plugin-postcss": "^2.1.20",
"gatsby-plugin-react-svg": "^3.0.0",
"gatsby-plugin-sass": "^2.1.28",
"gatsby-theme-docz": "^2.2.0",
"moment": "^2.24.0",
"node-sass": "^4.13.1",
"prop-types": "^15.7.2",
"react": "16.11.0",
"react-dates": "^21.8.0",
"react-dom": "16.11.0",
"react-powerplug": "1.0.0"
"react-powerplug": "1.0.0",
"theme-ui": "^0.2.38"
},
"devDependencies": {
"autoprefixer": "^9.7.4",

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="currentColor" fill-rule="evenodd">
<path d="M20 20h-4v-4h4v4zm-6-10h-4v4h4v-4zm6 0h-4v4h4v-4zm-12 6h-4v4h4v-4zm6 0h-4v4h4v-4zm-6-6h-4v4h4v-4zm16-8v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 509 B

View File

@ -0,0 +1,98 @@
.DateRangePicker {
}
/** CONTAINER STYLES **/
.DateRangePickerInput {
@apply bg-transparent border-0 flex;
}
.DateRangePicker_picker {
@apply -mt-1;
}
/** INPUT DIV STYLES **/
.DateInput {
background: transparent;
@apply flex flex-1 w-auto;
}
/** INPUT FIELD COMMON STYLES **/
.DateInput_input {
/* used data:image as background-image because svg import with relative url didn't work */
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="%236b6b6b" fill-rule="evenodd"><path d="M20 20h-4v-4h4v4zm-6-10h-4v4h4v-4zm6 0h-4v4h4v-4zm-12 6h-4v4h4v-4zm6 0h-4v4h4v-4zm-6-6h-4v4h4v-4zm16-8v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2z"/></g></svg>');
background-size: 14px;
background-position: 10px center;
@apply bg-no-repeat;
}
.DateInput_input {
@apply cursor-pointer border-custom-blue mt-2 bg-black shadow transition duration-300 appearance-none border-t border-l border-r border-b border-solid rounded w-full py-2 px-3 text-sm text-gray-700 leading-tight pl-8;
}
.DateInput_input:hover {
@apply border-gray-500;
}
.DateInput_input:focus {
@apply border-gray-500 outline-none;
}
/** FIRST INPUT STYLES **/
.DateInput:first-child .DateInput_input {
@apply rounded-r-none;
}
.DateInput:first-child .DateInput_input:hover,
.DateInput:first-child .DateInput_input:focus {
@apply relative z-10;
}
/** SECOND INPUT STYLES **/
.DateInput:last-child .DateInput_input {
@apply rounded-l-none;
margin-left: -1px;
}
/** ARROW STYLES **/
.DateRangePickerInput_arrow {
@apply hidden;
}
/* SELECT MONTH PICKER */
.DateRangePicker_select {
@apply bg-white text-custom-dark border rounded border-custom-gray appearance-none cursor-pointer text-base pl-2 pr-5 py-1; /* NEEDED FOR ARROW DOWN */
background-image: linear-gradient(45deg, transparent 50%, gray 50%),
linear-gradient(135deg, gray 50%, transparent 50%);
background-position: calc(100% - 11px) 11px, calc(100% - 6px) calc(11px);
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
/* CALENDAR DAYS */
.CalendarDay {
@apply border-0 rounded-full;
}
.CalendarDay:hover,
.CalendarDay__selected,
.CalendarDay__selected:active,
.CalendarDay__selected:hover {
@apply bg-custom-blue text-white border-0 border-custom-blue;
}
.CalendarDay__blocked_out_of_range:hover {
@apply border-0 bg-white cursor-not-allowed text-custom-gray;
}
.CalendarDay__selected_span {
@apply border-0 bg-custom-aquaBright;
}
/* MONTH NAVIGATION BUTTONS */
.DayPickerNavigation_button__horizontalDefault,
.DayPickerNavigation_button__horizontalDefault:hover {
@apply border-custom-gray text-custom-gray;
top: 24px;
padding: 3px 9px;
}
.DayPickerNavigation_svg__horizontal {
@apply fill-current;
}

View File

@ -0,0 +1,156 @@
/** REACT DATES */
import 'react-dates/initialize';
import 'react-dates/lib/css/_datepicker.css';
import { DateRangePicker, isInclusivelyBeforeDay } from 'react-dates';
import './DateRange.css';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
const DateRange = props => {
const { onChange, startDate, endDate } = props;
const [focusedInput, setFocusedInput] = useState(null);
const today = moment();
const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month');
const studyDatePresets = [
{
text: 'Today',
start: today,
end: today,
},
{
text: 'Last 7 days',
start: lastWeek,
end: today,
},
{
text: 'Last 30 days',
start: lastMonth,
end: today,
},
];
const renderDatePresets = () => {
return (
<div className="PresetDateRangePicker_panel flex justify-between">
{studyDatePresets.map(({ text, start, end }) => {
return (
<button
key={text}
type="button"
className={`m-0 py-2 px-3 bg-custom-blue border-0 rounded text-white text-base transition duration-300 hover:opacity-80`}
onClick={() =>
onChange({
startDate: start,
endDate: end,
preset: true,
})
}
>
{text}
</button>
);
})}
</div>
);
};
const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => {
const renderYearsOptions = () => {
const yearsRange = 20;
const options = [];
for (let i = 0; i < yearsRange; i++) {
const year = moment().year() - i;
options.push(
<option key={year} value={year}>
{year}
</option>
);
}
return options;
};
renderMonthElement.propTypes = {
month: PropTypes.object,
onMonthSelect: PropTypes.func,
onYearSelect: PropTypes.func,
};
return (
<div className="flex justify-center">
<div className="my-0 mx-1">
<select
className="DateRangePicker_select"
value={month.month()}
onChange={e => onMonthSelect(month, e.target.value)}
>
{moment.months().map((label, value) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<div className="my-0 mx-1">
{}
<select
className="DateRangePicker_select"
value={month.year()}
onChange={e => onYearSelect(month, e.target.value)}
>
{renderYearsOptions()}
</select>
</div>
</div>
);
};
return (
<DateRangePicker
/** REQUIRED */
startDate={startDate}
startDateId={'startDateId'}
endDate={endDate}
endDateId={'endDateId'}
onDatesChange={onChange}
focusedInput={focusedInput}
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
/** OPTIONAL */
renderCalendarInfo={renderDatePresets}
renderMonthElement={renderMonthElement}
startDatePlaceholderText={'Start Date'}
endDatePlaceholderText={'End Date'}
phrases={{
closeDatePicker: 'Close',
clearDates: 'Clear dates',
}}
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
hideKeyboardShortcutsPanel={true}
numberOfMonths={1}
showClearDates={false}
anchorDirection="left"
/>
);
};
DateRange.defaultProps = {
startDate: null,
endDate: null,
};
DateRange.propTypes = {
/** Start date moment object */
startDate: PropTypes.object, // moment date is an object
/** End date moment object */
endDate: PropTypes.object, // moment date is an object
/** Callback that returns on object with selected dates */
onChange: PropTypes.func.isRequired,
};
export default DateRange;

View File

@ -0,0 +1,45 @@
---
name: Date Range
menu: Components
route: components/date-range
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import DateRange from '../DateRange';
# Date Range
Date Range is used to select a range of dates.
## Import
```javascript
import { DateRange } from '@ohfi/ui';
```
## Date Range
<Playground>
{() => {
const [dates, setDates] = useState({
startDate: null,
endDate: null,
});
return (
<div className="p-4 flex flex-col items-center">
<DateRange
startDate={dates.startDate}
endDate={dates.endDate}
onChange={({ startDate, endDate }) =>
setDates({ startDate, endDate })
}
/>
</div>
);
}}
</Playground>
## Properties
<Props of={DateRange} />

View File

@ -0,0 +1,3 @@
import DateRange from './DateRange';
export default DateRange;

View File

@ -3,6 +3,7 @@ import React from 'react';
import arrowDown from './../../assets/icons/arrow-down.svg';
import seriesActive from './../../assets/icons/series-active.svg';
import seriesInactive from './../../assets/icons/series-inactive.svg';
import calendar from './../../assets/icons/calendar.svg';
import cancel from './../../assets/icons/cancel.svg';
import chevronDown from './../../assets/icons/chevron-down.svg';
import chevronRight from './../../assets/icons/chevron-right.svg';
@ -21,6 +22,7 @@ const ICONS = {
'arrow-down': arrowDown,
'series-active': seriesActive,
'series-inactive': seriesInactive,
calendar: calendar,
cancel: cancel,
'chevron-down': chevronDown,
'chevron-right': chevronRight,

View File

@ -20,6 +20,7 @@ const Input = ({
type = 'text',
value,
onChange,
onFocus,
...otherProps
}) => {
return (
@ -34,6 +35,7 @@ const Input = ({
type={type}
value={value}
onChange={onChange}
onFocus={onFocus}
{...otherProps}
/>
</div>
@ -49,6 +51,7 @@ Input.propTypes = {
type: PropTypes.string,
value: PropTypes.any,
onChange: PropTypes.func,
onFocus: PropTypes.func,
};
export default Input;

View File

@ -1,5 +1,6 @@
import Button from './Button';
import ButtonGroup from './ButtonGroup';
import DateRange from './DateRange';
import Icon from './Icon';
import IconButton from './IconButton';
import Input from './Input';
@ -10,6 +11,7 @@ import Typography from './Typography';
export {
Button,
ButtonGroup,
DateRange,
Icon,
IconButton,
Input,

View File

@ -25,16 +25,16 @@ const filtersMeta = [
{
name: 'studyDate',
displayName: 'Study date',
inputType: 'text',
inputType: 'date-range',
isSortable: true,
gridCol: 3,
gridCol: 5,
},
{
name: 'description',
displayName: 'Description',
inputType: 'text',
isSortable: true,
gridCol: 5,
gridCol: 4,
},
{
name: 'modality',
@ -55,7 +55,7 @@ const filtersMeta = [
displayName: 'Instances',
inputType: 'none',
isSortable: false,
gridCol: 3,
gridCol: 2,
},
];

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Button, Icon, Input, Typography } from '@ohif/ui';
import { Button, Icon, Input, Typography, DateRange } from '@ohif/ui';
const sortIconMap = {
'-1': 'sorting-active-down',
@ -14,7 +14,8 @@ const defaultProps = {
filtersValues: {
patientName: '',
mrn: '',
studyDate: '',
startDate: null,
endDate: null,
description: '',
modality: '',
accession: '',
@ -31,7 +32,6 @@ const FilterLabel = ({
isBeingSorted = false,
sortDirection = 0,
onLabelClick,
inputType,
className,
children,
}) => {
@ -70,6 +70,16 @@ const FilterLabel = ({
);
};
FilterLabel.propTypes = {
label: PropTypes.string,
isSortable: PropTypes.bool,
isBeingSorted: PropTypes.bool,
sortDirection: PropTypes.number,
onLabelClick: PropTypes.func,
className: PropTypes.string,
children: PropTypes.node,
};
const StudyListFilter = ({
filtersMeta = [],
filtersValues = defaultProps.filtersValues,
@ -107,22 +117,49 @@ const StudyListFilter = ({
};
const clearFilters = () => {
const _filterValues = { ...currentFiltersValues };
filtersMeta.forEach(filter => {
if (_filterValues[filter.name]) {
delete _filterValues[filter.name];
}
});
setcurrentFiltersValues(_filterValues);
setcurrentFiltersValues(defaultProps.filtersValues);
};
const isFiltering = () => {
return filtersMeta.some(filter => {
const filterValue = currentFiltersValues[filter.name];
return filterValue && filterValue !== '';
return Object.keys(currentFiltersValues).some(name => {
const filterValue = currentFiltersValues[name];
return filterValue !== defaultProps.filtersValues[name];
});
};
const renderInput = (inputType, name) => {
switch (inputType) {
case 'date-range': {
return (
<div className="relative">
<DateRange
startDate={currentFiltersValues.startDate}
endDate={currentFiltersValues.endDate}
onChange={({ startDate, endDate, preset = false }) => {
setcurrentFiltersValues(state => ({
...state,
startDate,
endDate,
}));
}}
/>
</div>
);
}
default:
return (
<Input
className="border-custom-blue mt-2 bg-black"
type="text"
containerClassName="mr-2"
value={currentFiltersValues[name] || ''}
onChange={event => handleFilterValueChange(event, name)}
/>
);
}
};
return (
<>
<div>
@ -203,17 +240,7 @@ const StudyListFilter = ({
onLabelClick={() => handleFilterLabelClick(name)}
inputType={inputType}
>
{inputType !== 'none' && (
<Input
className="border-custom-blue mt-2 bg-black"
type="text"
containerClassName="mr-2"
value={currentFiltersValues[name] || ''}
onChange={event =>
handleFilterValueChange(event, name)
}
/>
)}
{inputType !== 'none' && renderInput(inputType, name)}
</FilterLabel>
</div>
);
@ -237,15 +264,19 @@ const StudyListFilter = ({
};
StudyListFilter.propTypes = {
filtersMeta: PropTypes.arrayOf({
name: PropTypes.string,
dsplayName: PropTypes.string,
inputType: PropTypes.oneOf(['text', 'select', 'date-range', 'none']),
isSortable: PropTypes.bool,
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
}),
filtersMeta: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
dsplayName: PropTypes.string,
inputType: PropTypes.oneOf(['text', 'select', 'date-range', 'none']),
isSortable: PropTypes.bool,
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
})
),
filtersValues: PropTypes.object,
numOfStudies: PropTypes.number,
startDate: PropTypes.string,
endDate: PropTypes.string,
};
export default StudyListFilter;

View File

@ -16384,6 +16384,27 @@ react-dates@21.2.1:
react-with-styles "^4.0.1"
react-with-styles-interface-css "^6.0.0"
react-dates@^21.8.0:
version "21.8.0"
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.8.0.tgz#355c3c7a243a7c29568fe00aca96231e171a5e94"
integrity sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw==
dependencies:
airbnb-prop-types "^2.15.0"
consolidated-events "^1.1.1 || ^2.0.0"
enzyme-shallow-equal "^1.0.0"
is-touch-device "^1.0.1"
lodash "^4.1.1"
object.assign "^4.1.0"
object.values "^1.1.0"
prop-types "^15.7.2"
raf "^3.4.1"
react-moment-proptypes "^1.6.0"
react-outside-click-handler "^1.2.4"
react-portal "^4.2.0"
react-with-direction "^1.3.1"
react-with-styles "^4.1.0"
react-with-styles-interface-css "^6.0.0"
react-dev-utils@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-4.2.3.tgz#5b42d9ea58d5e9e017a2f57a40a8af408a3a46fb"
@ -16766,7 +16787,7 @@ react-with-styles-interface-css@^6.0.0:
array.prototype.flat "^1.2.1"
global-cache "^1.2.1"
react-with-styles@^4.0.1:
react-with-styles@^4.0.1, react-with-styles@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-4.1.0.tgz#4bfc2daa92dd72033fc19fd861b90225a682a640"
integrity sha512-zp05fyA6XFetqr07ox/a0bCFyEj//gUozI9cC1GW59zaGJ38STnxYvzotutgpzMyHOd7TFW9ZiZeBKjsYaS+RQ==