diff --git a/.eslintrc.js b/.eslintrc.js index 147ba164b..a3673eb6a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,30 +1,26 @@ module.exports = { - extends: [ - "eslint:recommended", - "plugin:react/recommended", - "prettier", + extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'], + plugins: ['react', 'prettier'], + rules: { + 'prettier/prettier': [ + 'error', + { + singleQuote: true, + trailingComma: 'es5', + bracketSpacing: true, + jsxBracketSameLine: true, + printWidth: 100, + parser: 'babylon' + } ], - plugins: [ - "react", - "prettier", - ], - rules: { - "prettier/prettier": ["error", { - "singleQuote": true, - "trailingComma": "es5", - "bracketSpacing": true, - "jsxBracketSameLine": true, - "printWidth": 100, - "parser": "babylon", - }], - "no-debugger": 0, - "no-console": 0, - }, - parser: "babel-eslint", - env: { - "es6": true, - "node": true, - "browser": true, - "jest": true, - }, -}; \ No newline at end of file + 'no-debugger': 0, + 'no-console': 0 + }, + parser: 'babel-eslint', + env: { + es6: true, + node: true, + browser: true, + jest: true + } +}; diff --git a/.github/workflows/publish_to_npm.yml b/.github/workflows/publish_to_npm.yml index 4ee8e378b..ef071a1fe 100644 --- a/.github/workflows/publish_to_npm.yml +++ b/.github/workflows/publish_to_npm.yml @@ -18,6 +18,15 @@ jobs: - run: yarn build:css - run: yarn build:js - run: yarn styleguidist build - - run: yarn publish + - name: Get version from package.json + id: get_version + run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - name: Publish to npm + run: | + if [[ "${{ steps.get_version.outputs.VERSION }}" == *"alpha"* ]]; then + yarn publish --tag alpha + else + yarn publish + fi env: NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 31ca39e05..48f8c6540 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode/ node_modules/ .DS_Store *.log diff --git a/.prettierrc b/.prettierrc index 727df7cf8..9829e768e 100755 --- a/.prettierrc +++ b/.prettierrc @@ -1,11 +1,8 @@ { - "printWidth": 110, - "tabWidth": 2, - "useTabs": false, - "semi": true, - "singleQuote": true, - "trailingComma": "none", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "fluid": false - } \ No newline at end of file + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "printWidth": 100, + "parser": "babylon" +} diff --git a/package.json b/package.json index f2bb2047f..c068318db 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "silq-react-date-range", - "version": "2.4.0", + "version": "2.4.1-alpha.8", "description": "A React component for choosing dates and date ranges.", "main": "dist/index.js", "scripts": { - "start": "yarn build:css & styleguidist server", + "start": "yarn watch:css & yarn watch:js & styleguidist server", "build": "yarn build:css & yarn build:js & ls & styleguidist build", + "watch:css": "postcss 'src/styles.scss' -w -d dist --ext css & postcss 'src/theme/*.scss' -w -d 'dist/theme' --ext css", "build:css": "postcss 'src/styles.scss' -d dist --ext css & postcss 'src/theme/*.scss' -d 'dist/theme' --ext css", "build:js": "babel ./src --out-dir ./dist --ignore test.js", + "watch:js": "babel ./src --out-dir ./dist --ignore test.js --watch", "lint": "eslint 'src/**/*.js'", "test": "jest", "preversion": "yarn clear & yarn build" diff --git a/src/components/Calendar/index.js b/src/components/Calendar/index.js index 7f683af64..bea269fbd 100644 --- a/src/components/Calendar/index.js +++ b/src/components/Calendar/index.js @@ -1,9 +1,9 @@ +// @ts-check import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { rangeShape } from '../DayCell'; import Month from '../Month'; -import DateInput from '../DateInput'; -import { calcFocusDate, generateStyles, getMonthDisplayRange } from '../../utils'; +import { calcFocusDate, CommonProps, generateStyles, getMonthDisplayRange } from '../../utils'; import classnames from 'classnames'; import ReactList from 'react-list'; import { shallowEqualObjects } from 'shallow-equal'; @@ -15,7 +15,6 @@ import { startOfWeek, endOfWeek, isSameDay, - addYears, setYear, setMonth, differenceInCalendarMonths, @@ -25,15 +24,15 @@ import { isSameMonth, differenceInDays, min, - max + max, } from 'date-fns'; import { enUS as defaultLocale } from 'date-fns/locale/en-US'; import coreStyles from '../../styles'; import { ariaLabelsShape } from '../../accessibility'; class Calendar extends PureComponent { - constructor (props, context) { - super(props, context); + constructor(props) { + super(props); this.dateOptions = { locale: props.locale }; if (props.weekStartsOn !== undefined) this.dateOptions.weekStartsOn = props.weekStartsOn; this.styles = generateStyles([coreStyles, props.classNames]); @@ -45,16 +44,16 @@ class Calendar extends PureComponent { drag: { status: false, range: { startDate: null, endDate: null }, - disablePreview: false + disablePreview: false, }, - scrollArea: this.calcScrollArea(props) + scrollArea: this.calcScrollArea(props), }; } - getMonthNames () { + getMonthNames() { return [...Array(12).keys()].map(i => this.props.locale.localize.month(i)); } - calcScrollArea (props) { + calcScrollArea(props) { const { direction, months, scroll } = props; if (!scroll.enabled) return { enabled: false }; @@ -65,7 +64,7 @@ class Calendar extends PureComponent { monthHeight: scroll.monthHeight || 220, longMonthHeight: longMonthHeight || 260, calendarWidth: 'auto', - calendarHeight: (scroll.calendarHeight || longMonthHeight || 240) * months + calendarHeight: (scroll.calendarHeight || longMonthHeight || 240) * months, }; } return { @@ -73,7 +72,7 @@ class Calendar extends PureComponent { monthWidth: scroll.monthWidth || 332, calendarWidth: (scroll.calendarWidth || scroll.monthWidth || 332) * months, monthHeight: longMonthHeight || 300, - calendarHeight: longMonthHeight || 300 + calendarHeight: longMonthHeight || 300, }; } focusToDate = (date, props = this.props, preventUnnecessary = true) => { @@ -100,7 +99,7 @@ class Calendar extends PureComponent { const newProps = props.scroll.enabled ? { ...props, - months: this.list.getVisibleRange().length + months: this.list.getVisibleRange().length, } : props; const newFocus = calcFocusDate(this.state.focusedDate, newProps); @@ -114,32 +113,36 @@ class Calendar extends PureComponent { const preview = { startDate: val, endDate: val, - color: this.props.color + color: this.props.color, }; this.setState({ preview }); }; - componentDidMount () { + componentDidMount() { if (this.props.scroll.enabled) { // prevent react-list's initial render focus problem setTimeout(() => this.focusToDate(this.state.focusedDate)); } } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { const propMapper = { dateRange: 'ranges', - date: 'date' + date: 'date', }; const targetProp = propMapper[this.props.displayMode]; if (this.props[targetProp] !== prevProps[targetProp]) { this.updateShownDate(this.props); } - if (prevProps.locale !== this.props.locale || prevProps.weekStartsOn !== this.props.weekStartsOn) { + if ( + prevProps.locale !== this.props.locale || + prevProps.weekStartsOn !== this.props.weekStartsOn + ) { this.dateOptions = { locale: this.props.locale }; - if (this.props.weekStartsOn !== undefined) this.dateOptions.weekStartsOn = this.props.weekStartsOn; + if (this.props.weekStartsOn !== undefined) + this.dateOptions.weekStartsOn = this.props.weekStartsOn; this.setState({ - monthNames: this.getMonthNames() + monthNames: this.getMonthNames(), }); } @@ -155,24 +158,22 @@ class Calendar extends PureComponent { monthOffset: () => addMonths(focusedDate, value), setMonth: () => setMonth(focusedDate, value), setYear: () => setYear(focusedDate, value), - set: () => value + set: () => value, }; const newDate = min([max([modeMapper[mode](), minDate]), maxDate]); this.focusToDate(newDate, this.props, false); onShownDateChange && onShownDateChange(newDate); }; - handleRangeFocusChange = (rangesIndex, rangeItemIndex) => { - this.props.onRangeFocusChange && this.props.onRangeFocusChange([rangesIndex, rangeItemIndex]); - }; + handleScroll = () => { const { onShownDateChange, minDate } = this.props; const { focusedDate } = this.state; const { isFirstRender } = this; - const visibleMonths = this.list.getVisibleRange(); + const visibleMonths = this.list?.getVisibleRange(); // prevent scroll jump with wrong visible value - if (visibleMonths[0] === undefined) return; + if (!visibleMonths || visibleMonths[0] === undefined) return; const visibleMonth = addMonths(minDate, visibleMonths[0] || 0); const isFocusedToDifferent = !isSameMonth(visibleMonth, focusedDate); if (isFocusedToDifferent && !isFirstRender) { @@ -181,97 +182,124 @@ class Calendar extends PureComponent { } this.isFirstRender = false; }; + renderMonthAndYear = (focusedDate, changeShownDate, props) => { - const { showMonthArrow, minDate, maxDate, showMonthAndYearPickers, ariaLabels } = props; + const { minDate, maxDate, ariaLabels } = props; const upperYearLimit = (maxDate || Calendar.defaultProps.maxDate).getFullYear(); const lowerYearLimit = (minDate || Calendar.defaultProps.minDate).getFullYear(); + const styles = this.styles; + + return ( +
+ {this.state.monthNames[focusedDate.getMonth()].slice(0, 3).toUpperCase()} + + + + +
+ ); + }; + + renderNavigation = (focusedDate, changeShownDate, props) => { + const { showMonthArrow, ariaLabels, months } = props; + const styles = this.styles; return (
e.stopPropagation()} className={styles.monthAndYearWrapper}> - {showMonthArrow ? ( - - ) : null} - {showMonthAndYearPickers ? ( +
+ {showMonthArrow ? ( + + ) : null} + {this.renderMonthAndYear(focusedDate, changeShownDate, props)} +
+ {/* {showMonthAndYearPickers ? ( - - - - + {this.renderMonthAndYear(addMonths(focusedDate, 1), changeShownDate, props)} ) : ( {this.state.monthNames[focusedDate.getMonth()]} {focusedDate.getFullYear()} - )} - {showMonthArrow ? ( - - ) : null} + )} */} +
+ {months === 2 && + this.renderMonthAndYear(addMonths(focusedDate, 1), changeShownDate, props)} + {showMonthArrow ? ( + + ) : null} +
); }; - renderWeekdays () { + renderWeekdays() { const now = new Date(); return (
{eachDayOfInterval({ start: startOfWeek(now, this.dateOptions), - end: endOfWeek(now, this.dateOptions) + end: endOfWeek(now, this.dateOptions), }).map((day, i) => ( {format(day, this.props.weekdayDisplayFormat, this.dateOptions)} @@ -280,70 +308,6 @@ class Calendar extends PureComponent {
); } - renderDateDisplay = () => { - const { - focusedRange, - color, - ranges, - rangeColors, - dateDisplayFormat, - editableDateInputs, - startDatePlaceholder, - endDatePlaceholder, - ariaLabels - } = this.props; - - const defaultColor = rangeColors[focusedRange[0]] || color; - const styles = this.styles; - - return ( -
- {ranges.map((range, i) => { - if (range.showDateDisplay === false || (range.disabled && !range.showDateDisplay)) return null; - return ( -
- this.handleRangeFocusChange(i, 0)} - /> - this.handleRangeFocusChange(i, 1)} - /> -
- ); - })} -
- ); - }; onDragSelectionStart = date => { const { onChange, dragSelectionEnabled } = this.props; @@ -352,8 +316,8 @@ class Calendar extends PureComponent { drag: { status: true, range: { startDate: date, endDate: date }, - disablePreview: true - } + disablePreview: true, + }, }); } else { onChange && onChange(date); @@ -371,7 +335,7 @@ class Calendar extends PureComponent { } const newRange = { startDate: this.state.drag.range.startDate, - endDate: date + endDate: date, }; if (displayMode !== 'dateRange' || isSameDay(newRange.startDate, date)) { this.setState({ drag: { status: false, range: {} } }, () => onChange && onChange(date)); @@ -388,8 +352,8 @@ class Calendar extends PureComponent { drag: { status: drag.status, range: { startDate: drag.range.startDate, endDate: date }, - disablePreview: true - } + disablePreview: true, + }, }); }; @@ -406,9 +370,8 @@ class Calendar extends PureComponent { const isLongMonth = differenceInDays(end, start, this.dateOptions) + 1 > 7 * 5; return isLongMonth ? scrollArea.longMonthHeight : scrollArea.monthHeight; }; - render () { + render() { const { - showDateDisplay, onPreviewChange, scroll, direction, @@ -420,15 +383,15 @@ class Calendar extends PureComponent { color, navigatorRenderer, className, - preview + preview, } = this.props; const { scrollArea, focusedDate } = this.state; const isVertical = direction === 'vertical'; - const monthAndYearRenderer = navigatorRenderer || this.renderMonthAndYear; + const monthAndYearRenderer = navigatorRenderer || this.renderNavigation; const ranges = this.props.ranges.map((range, i) => ({ ...range, - color: range.color || rangeColors[i] || color + color: range.color || rangeColors[i] || color, })); return (
this.setState({ drag: { status: false, range: {} } })} onMouseLeave={() => { this.setState({ drag: { status: false, range: {} } }); - }} - > - {showDateDisplay && this.renderDateDisplay()} + }}> {monthAndYearRenderer(focusedDate, this.changeShownDate, this.props)} {scroll.enabled ? (
@@ -451,10 +412,9 @@ class Calendar extends PureComponent { onMouseLeave={() => onPreviewChange && onPreviewChange()} style={{ width: scrollArea.calendarWidth + 11, - height: scrollArea.calendarHeight + 11 + height: scrollArea.calendarHeight + 11, }} - onScroll={this.handleScroll} - > + onScroll={this.handleScroll}> (this.list = target)} itemSizeEstimator={this.estimateMonthSize} axis={isVertical ? 'y' : 'x'} @@ -503,8 +463,7 @@ class Calendar extends PureComponent { className={classnames( this.styles.months, isVertical ? this.styles.monthsVertical : this.styles.monthsHorizontal - )} - > + )}> {new Array(this.props.months).fill(null).map((_, i) => { let monthStep = addMonths(this.state.focusedDate, i); if (this.props.calendarFocus === 'backwards') { @@ -528,7 +487,7 @@ class Calendar extends PureComponent { onMouseLeave={() => onPreviewChange && onPreviewChange()} styles={this.styles} showWeekDays={!isVertical || i === 0} - showMonthName={!isVertical || i > 0} + // showMonthName={!isVertical || i > 0} /> ); })} @@ -552,26 +511,24 @@ Calendar.defaultProps = { monthDisplayFormat: 'MMM yyyy', weekdayDisplayFormat: 'EEEEE', dayDisplayFormat: 'd', - showDateDisplay: true, showPreview: true, displayMode: 'date', months: 1, color: '#334bfa', scroll: { - enabled: false + enabled: false, }, direction: 'vertical', - maxDate: addYears(new Date(), 20), - minDate: addYears(new Date(), -100), rangeColors: ['#334bfa', '#3ecf8e', '#fed14c'], - startDatePlaceholder: 'Early', - endDatePlaceholder: 'Continuous', + startDatePlaceholder: 'Start Date', + endDatePlaceholder: 'End Date', editableDateInputs: false, dragSelectionEnabled: true, fixedHeight: false, calendarFocus: 'forwards', preventSnapRefocus: false, - ariaLabels: {} + ariaLabels: {}, + ...CommonProps, }; Calendar.propTypes = { @@ -593,7 +550,7 @@ Calendar.propTypes = { preview: PropTypes.shape({ startDate: PropTypes.object, endDate: PropTypes.object, - color: PropTypes.string + color: PropTypes.string, }), dateDisplayFormat: PropTypes.string, monthDisplayFormat: PropTypes.string, @@ -615,7 +572,7 @@ Calendar.propTypes = { longMonthHeight: PropTypes.number, monthWidth: PropTypes.number, calendarWidth: PropTypes.number, - calendarHeight: PropTypes.number + calendarHeight: PropTypes.number, }), direction: PropTypes.oneOf(['vertical', 'horizontal']), startDatePlaceholder: PropTypes.string, @@ -627,7 +584,7 @@ Calendar.propTypes = { fixedHeight: PropTypes.bool, calendarFocus: PropTypes.string, preventSnapRefocus: PropTypes.bool, - ariaLabels: ariaLabelsShape + ariaLabels: ariaLabelsShape, }; export default Calendar; diff --git a/src/components/Calendar/index.scss b/src/components/Calendar/index.scss index dbd97b41f..35070db8c 100644 --- a/src/components/Calendar/index.scss +++ b/src/components/Calendar/index.scss @@ -6,19 +6,10 @@ user-select: none; } -.rdrDateDisplay{ - display: flex; - justify-content: space-between; -} - .rdrDateDisplayItem{ flex: 1 1; width: 0; - text-align: center; color: inherit; - & + &{ - margin-left: 0.833em; - } input{ text-align: inherit; &:disabled{ @@ -27,6 +18,9 @@ } } +.rdrDateDisplayItem.condition { + flex: 0 0; +} .rdrMonthAndYearWrapper { @@ -35,10 +29,10 @@ justify-content: space-between; } -.rdrMonthAndYearPickers{ +.rdrMonthAndYearPickers{ /* change to .rdrNavigationHeader if used */ flex: 1 1 auto; display: flex; - justify-content: center; + justify-content: space-around; align-items: center; } @@ -73,15 +67,17 @@ } .rdrMonth{ - width: 17.5em; + width: 14rem; } + .rdrWeekDays{ display: flex; } .rdrWeekDay { flex-basis: calc(100% / 7); + height: 36px; box-sizing: inherit; text-align: center; } @@ -91,8 +87,6 @@ flex-wrap: wrap; } -.rdrDateDisplayWrapper{} - .rdrMonthName{} .rdrInfiniteMonths{ diff --git a/src/components/DateInput/index.js b/src/components/DateInput/index.js index 8263e4f61..0656c53dd 100644 --- a/src/components/DateInput/index.js +++ b/src/components/DateInput/index.js @@ -1,48 +1,132 @@ +// @ts-check import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { format, parse, isValid, isEqual } from 'date-fns'; +import { format, parse, isValid, isEqual, isBefore, isAfter } from 'date-fns'; +import CalendarIcon from '../../icons/Calendar'; + +export class DateConditionInput extends PureComponent { + constructor(props) { + super(props); + + this.state = { + invalid: false, + changed: false, + }; + } + + render = () => { + const { invalid } = this.state; + const { condition, onConditionChange } = this.props; + + return ( +
+ +
+ +
+ {invalid && } +
+ ); + }; +} + +DateConditionInput.defaultProps = { + availableConditions: ['between', 'on', 'before', 'after'], +}; + +DateConditionInput.propTypes = { + id: PropTypes.string, + ariaLabel: PropTypes.string, + onFocus: PropTypes.func, + condition: PropTypes.string, + onConditionChange: PropTypes.func, + availableConditions: PropTypes.array, + className: PropTypes.string, +}; class DateInput extends PureComponent { - constructor (props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { invalid: false, + error: '', changed: false, - value: this.formatDate(props) + value: this.formatDate(props), }; } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { const { value } = prevProps; if (!isEqual(value, this.props.value)) { - this.setState({ value: this.formatDate(this.props) }); + const { isValid, error } = this.checkValidity(this.props.value); + this.setState({ + value: this.formatDate(this.props), + invalid: !isValid, + error, + changed: false, + }); } } - formatDate ({ value, dateDisplayFormat, dateOptions }) { + formatDate({ value, dateDisplayFormat, dateOptions }) { if (value && isValid(value)) { return format(value, dateDisplayFormat, dateOptions); } return ''; } - update (value) { + parse(value) { + const { dateDisplayFormat, dateOptions } = this.props; + return parse(value, dateDisplayFormat, new Date(), dateOptions); + } + + checkValidity(value) { + const { minDate, maxDate, label, dateDisplayFormat, dateOptions } = this.props; + const parsed = typeof value === 'string' ? this.parse(value) : value; + if (!isValid(parsed)) { + return { isValid: false, error: 'Please enter a valid date' }; + } + if (minDate && !isAfter(parsed, minDate)) { + return { + isValid: false, + error: `${label} should be after ${format(minDate, dateDisplayFormat, dateOptions)}`, + }; + } + if (maxDate && !isBefore(parsed, maxDate)) { + return { + isValid: false, + error: `${label} should be before ${format(maxDate, dateDisplayFormat, dateOptions)}`, + }; + } + return { isValid: true }; + } + + update(value) { const { invalid, changed } = this.state; if (invalid || !changed || !value) { return; } - const { onChange, dateDisplayFormat, dateOptions } = this.props; - const parsed = parse(value, dateDisplayFormat, new Date(), dateOptions); - - if (isValid(parsed)) { + const { onChange } = this.props; + const parsed = this.parse(value); + const { isValid, error } = this.checkValidity(value); + if (isValid) { this.setState({ changed: false }, () => onChange(parsed)); } else { - this.setState({ invalid: true }); + this.setState({ invalid: true, error }, () => onChange(parsed, !invalid, error)); } } @@ -63,30 +147,48 @@ class DateInput extends PureComponent { this.update(value); }; - render () { - const { className, readOnly, placeholder, ariaLabel, disabled, onFocus } = this.props; - const { value, invalid } = this.state; + render() { + const { + className, + readOnly, + placeholder, + ariaLabel, + disabled, + onFocus, + label, + id, + } = this.props; + const { value, invalid, error } = this.state; return ( - - - {invalid && } - +
+ +
+
+ +
+ +
+ {invalid &&
{error || 'Please enter a valid date'}
} +
); } } DateInput.propTypes = { + label: PropTypes.string, + id: PropTypes.string, value: PropTypes.object, placeholder: PropTypes.string, disabled: PropTypes.bool, @@ -95,14 +197,18 @@ DateInput.propTypes = { dateDisplayFormat: PropTypes.string, ariaLabel: PropTypes.string, className: PropTypes.string, - onFocus: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired + onFocus: PropTypes.func, // was required + onChange: PropTypes.func, // was required + minDate: PropTypes.object, + maxDate: PropTypes.object, }; DateInput.defaultProps = { + label: 'Date', + id: '', readOnly: true, disabled: false, - dateDisplayFormat: 'MMM D, YYYY' + dateDisplayFormat: 'MMM D, YYYY', }; export default DateInput; diff --git a/src/components/DateInput/index.scss b/src/components/DateInput/index.scss index 9bdf0ddb9..6d9ea8724 100644 --- a/src/components/DateInput/index.scss +++ b/src/components/DateInput/index.scss @@ -1,16 +1,69 @@ -.rdrDateInput { - position: relative; +.rdrDateInputContainer { + display: flex; + flex-direction: column; + gap: 6px; - input { + label { + color: #6C6C6C; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: normal; + } + + .rdrError { + /* position: absolute; */ + font-size: 12px; + line-height: 16px; + /* top: 0; */ + /* right: .25em; */ + color: #A61B25; + } +} + +.rdrDateInputContainer.condition { + width: 18%; + min-width: 90px; +} + +.rdrDateInputContainer { + .rdrDateInput { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.375rem; + border-radius: 5px; + border: 1px solid rgb(177 177 177); + padding: 0.25rem 0.375rem; + color: rgb(105 112 119); + + div { + line-height: 10px; + } + } + + input, select { + -webkit-text-size-adjust: 100%; + flex-grow: 1; + font-size: 14px; outline: none; + margin: 0; + padding: 0; + border-style: none; + outline: 2px solid transparent; + outline-offset: 2px; + background: none; + color: #202020; } - .rdrWarning { - position: absolute; - font-size: 1.6em; - line-height: 1.6em; - top: 0; - right: .25em; - color: #FF0000; + select { + width: 100%; + appearance: none; + -webkit-appearance: none; + background: transparent; + outline: 0; + background: url("data:image/svg+xml;utf8,") no-repeat; + background-position: right 0px center; + cursor: pointer; } -} \ No newline at end of file +} diff --git a/src/components/DateInputGroup/index.js b/src/components/DateInputGroup/index.js new file mode 100644 index 000000000..4a333f697 --- /dev/null +++ b/src/components/DateInputGroup/index.js @@ -0,0 +1,194 @@ +// @ts-check +import React, { PureComponent } from 'react'; +import DateInput, { DateConditionInput } from '../DateInput'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import coreStyles from '../../styles'; +import { generateStyles } from '../../utils'; +import { rangeShape } from '../DayCell'; +import { ariaLabelsShape } from '../../accessibility'; +import { enUS as defaultLocale } from 'date-fns/locale/en-US'; + +class DateInputGroup extends PureComponent { + constructor(props) { + super(props); + this.styles = generateStyles([coreStyles, props.classNames]); + this.dateOptions = { locale: props.locale }; + } + + handleRangeFocusChange = (rangesIndex, rangeItemIndex) => { + this.props.onRangeFocusChange && this.props.onRangeFocusChange([rangesIndex, rangeItemIndex]); + }; + + renderDateInputsBasedOnCondition = (range, i) => { + const styles = this.styles; + const { + focusedRange, + dateDisplayFormat, + editableDateInputs, + ariaLabels, + condition, + inputLabels, + inputPlaceholders, + } = this.props; + + switch (condition) { + case 'between': + return ( +
+ this.handleRangeFocusChange(i, 0)} + /> + this.handleRangeFocusChange(i, 1)} + /> +
+ ); + case 'before': + case 'after': + case 'on': + return ( + this.handleRangeFocusChange(i, 0)} + /> + ); + } + }; + + render = () => { + const { focusedRange, color, ranges, rangeColors, condition, onConditionChange } = this.props; + + const defaultColor = rangeColors[focusedRange[0]] || color; + const styles = this.styles; + + return ranges.map((range, i) => { + if (range.showDateDisplay === false || (range.disabled && !range.showDateDisplay)) + return null; + return ( +
+ + {this.renderDateInputsBasedOnCondition(range, i)} +
+ ); + }); + }; +} + +DateInputGroup.defaultProps = { + focusedRange: [0, 0], + locale: defaultLocale, + color: '#334bfa', + ranges: [], + rangeColors: ['#334bfa', '#3ecf8e', '#fed14c'], + ariaLabels: {}, + dateDisplayFormat: 'MM/d/yyyy', + editableDateInputs: false, + startDatePlaceholder: 'Start Date', + endDatePlaceholder: 'End Date', +}; + +export const ExposedDateInputProps = { + condition: PropTypes.string, + availableConditions: PropTypes.array, + minDate: PropTypes.object, + maxDate: PropTypes.object, + inputLabels: PropTypes.shape({ + between: PropTypes.shape({ + startDate: PropTypes.string, + endDate: PropTypes.string, + }), + on: PropTypes.string, + before: PropTypes.string, + after: PropTypes.string, + }), + inputPlaceholders: PropTypes.shape({ + between: PropTypes.shape({ + startDate: PropTypes.string, + endDate: PropTypes.string, + }), + on: PropTypes.string, + before: PropTypes.string, + after: PropTypes.string, + }), +}; + +DateInputGroup.propTypes = { + classNames: PropTypes.object, + locale: PropTypes.object, + focusedRange: PropTypes.arrayOf(PropTypes.number), + color: PropTypes.string, + ranges: PropTypes.arrayOf(rangeShape), + rangeColors: PropTypes.arrayOf(PropTypes.string), + dateDisplayFormat: PropTypes.string, + editableDateInputs: PropTypes.bool, + ariaLabels: ariaLabelsShape, + onDragSelectionEnd: PropTypes.func, + handleRangeFocusChange: PropTypes.func, + onConditionChange: PropTypes.func, + ...ExposedDateInputProps, +}; + +export default DateInputGroup; diff --git a/src/components/DateInputGroup/index.scss b/src/components/DateInputGroup/index.scss new file mode 100644 index 000000000..1b02e8b8b --- /dev/null +++ b/src/components/DateInputGroup/index.scss @@ -0,0 +1,19 @@ +.rdrDateDisplayWrapper { + display: flex; + gap: 0.75rem; + align-self: stretch; + width: 100%; +} + +.rdrDateDisplayWrapper.stackDateInputs { + flex-direction: column; + .rdrDateDisplayItem.condition { + width: 100%; + } +} + +.rdrBetweenDateInputsWrapper { + display: flex; + gap: 0.75rem; + width: 100%; +} \ No newline at end of file diff --git a/src/components/DateRange/index.js b/src/components/DateRange/index.js index d6db82386..1277f3f29 100644 --- a/src/components/DateRange/index.js +++ b/src/components/DateRange/index.js @@ -1,18 +1,19 @@ +// @ts-check import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Calendar from '../Calendar'; import { rangeShape } from '../DayCell'; -import { findNextRangeIndex, generateStyles } from '../../utils'; +import { CommonProps, findNextRangeIndex, generateStyles } from '../../utils'; import { isBefore, differenceInCalendarDays, addDays, min, isWithinInterval, max } from 'date-fns'; import classnames from 'classnames'; import coreStyles from '../../styles'; class DateRange extends Component { - constructor (props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { focusedRange: props.initialFocusedRange || [findNextRangeIndex(props.ranges), 0], - preview: null + preview: null, }; this.styles = generateStyles([coreStyles, props.classNames]); } @@ -24,7 +25,8 @@ class DateRange extends Component { maxDate, moveRangeOnFirstSelection, retainEndDateOnFirstSelection, - disabledDates + disabledDates, + displayMode, } = this.props; const focusedRangeIndex = focusedRange[0]; const selectedRange = ranges[focusedRangeIndex]; @@ -32,7 +34,11 @@ class DateRange extends Component { let { startDate, endDate } = selectedRange; const now = new Date(); let nextFocusRange; - if (!isSingleValue) { + + if (displayMode === 'date') { + startDate = value; + endDate = value; + } else if (!isSingleValue) { startDate = value.startDate; endDate = value.endDate; } else if (focusedRange[1] === 0) { @@ -68,7 +74,7 @@ class DateRange extends Component { const inValidDatesWithinRange = disabledDates.filter(disabledDate => isWithinInterval(disabledDate, { start: startDate, - end: endDate + end: endDate, }) ); @@ -87,7 +93,7 @@ class DateRange extends Component { return { wasValid: !(inValidDatesWithinRange.length > 0), range: { startDate, endDate }, - nextFocusRange: nextFocusRange + nextFocusRange: nextFocusRange, }; }; setSelection = (value, isSingleValue) => { @@ -100,12 +106,12 @@ class DateRange extends Component { onChange({ [selectedRange.key || `range${focusedRangeIndex + 1}`]: { ...selectedRange, - ...newSelection.range - } + ...newSelection.range, + }, }); this.setState({ focusedRange: newSelection.nextFocusRange, - preview: null + preview: null, }); onRangeFocusChange && onRangeFocusChange(newSelection.nextFocusRange); }; @@ -120,10 +126,15 @@ class DateRange extends Component { } const { rangeColors, ranges } = this.props; const focusedRange = this.props.focusedRange || this.state.focusedRange; - const color = ranges[focusedRange[0]]?.color || rangeColors[focusedRange[0]] || color; + const color = + ranges[focusedRange[0]]?.color || rangeColors[focusedRange[0]] || this.props.color; this.setState({ preview: { ...val.range, color } }); }; - render () { + + onDragSelectionEnd = date => { + this.calendar?.onDragSelectionEnd(date); + }; + render() { return ( this.setSelection(val, false)} @@ -151,7 +161,9 @@ DateRange.defaultProps = { moveRangeOnFirstSelection: false, retainEndDateOnFirstSelection: false, rangeColors: ['#334bfa', '#3ecf8e', '#fed14c'], - disabledDates: [] + disabledDates: [], + displayMode: 'dateRange', + ...CommonProps, }; DateRange.propTypes = { @@ -161,7 +173,7 @@ DateRange.propTypes = { className: PropTypes.string, ranges: PropTypes.arrayOf(rangeShape), moveRangeOnFirstSelection: PropTypes.bool, - retainEndDateOnFirstSelection: PropTypes.bool + retainEndDateOnFirstSelection: PropTypes.bool, }; export default DateRange; diff --git a/src/components/DateRange/index.scss b/src/components/DateRange/index.scss index 89dcf26d2..d41c9ad29 100644 --- a/src/components/DateRange/index.scss +++ b/src/components/DateRange/index.scss @@ -1,3 +1,4 @@ .rdrDateRangeWrapper{ user-select: none; + border-bottom-right-radius: 0.5rem; } diff --git a/src/components/DateRangePicker/index.js b/src/components/DateRangePicker/index.js index 827e55486..77b9af052 100644 --- a/src/components/DateRangePicker/index.js +++ b/src/components/DateRangePicker/index.js @@ -1,52 +1,165 @@ +// @ts-check import React, { Component } from 'react'; +import { startOfDay, isEqual } from 'date-fns'; import PropTypes from 'prop-types'; import DateRange from '../DateRange'; import DefinedRange from '../DefinedRange'; -import { findNextRangeIndex, generateStyles } from '../../utils'; +import { CommonProps, findNextRangeIndex, generateStyles } from '../../utils'; import classnames from 'classnames'; import coreStyles from '../../styles'; +import DateInputGroup, { ExposedDateInputProps } from '../DateInputGroup'; class DateRangePicker extends Component { - constructor (props) { + constructor(props) { super(props); this.state = { - focusedRange: [findNextRangeIndex(props.ranges), 0] + focusedRange: [findNextRangeIndex(props.ranges), 0], + condition: this.props.condition, }; this.styles = generateStyles([coreStyles, props.classNames]); + this.stackDateInputs = this.props.months === 1 && !this.props.showPresets; } - render () { + + onChange = value => { + Object.keys(value).forEach(key => { + value[key].condition = this.state.condition; + }); + this.props.onChange(value); + }; + + handleRangeFocusChange = focusedRange => { + this.setState({ focusedRange }); + }; + + onDragSelectionEnd = date => { + this.dateRange?.onDragSelectionEnd(date); + }; + + render() { + const { title, showPresets } = this.props; const { focusedRange } = this.state; return ( -
- this.setState({ focusedRange })} - focusedRange={focusedRange} - {...this.props} - ref={t => (this.dateRange = t)} - className={undefined} - /> - - this.dateRange.updatePreview( - value ? this.dateRange.calcNewSelection(value, typeof value === 'string') : null - ) - } - {...this.props} - range={this.props.ranges[focusedRange[0]]} - className={undefined} - /> +
+
+ {title && {title}} + { + this.setState({ condition }, () => { + this.onChange( + this.props.ranges.reduce((ranges, currentRange) => { + ranges[currentRange.key] = { + ...currentRange, + condition: this.state.condition, + }; + return ranges; + }, {}) + ); + }); + }} + availableConditions={this.props.availableConditions} + classNames={ + this.stackDateInputs + ? { + dateDisplayWrapper: classnames( + this.styles.dateDisplayWrapper, + 'stackDateInputs' + ), + } + : {} + } + /> +
+
+ {showPresets && ( + + this.dateRange?.updatePreview( + value ? this.dateRange?.calcNewSelection(value, typeof value === 'string') : null + ) + } + {...this.props} + onChange={value => { + const key = this.props.ranges[0].key; + this.setState( + { + condition: isEqual( + startOfDay(value[key].startDate), + startOfDay(value[key].endDate) + ) + ? 'on' + : 'between', + }, + () => { + this.onChange(value); + } + ); + }} + range={this.props.ranges[focusedRange[0]]} + className={undefined} + /> + )} + { + this.dateRange = t; + }} + className={undefined} + /> +
); } } -DateRangePicker.defaultProps = {}; +DateRangePicker.defaultProps = { + showDateDisplay: true, + condition: 'between', + showPresets: true, + inputLabels: { + between: { + startDate: 'Start date', + endDate: 'End date', + }, + on: 'Date', + before: 'End date', + after: 'Start date', + }, + inputPlaceholders: { + between: { + startDate: 'Start date', + endDate: 'End date', + }, + on: 'Date', + before: 'End date', + after: 'Start date', + }, + ...CommonProps, +}; DateRangePicker.propTypes = { + title: PropTypes.string, + id: PropTypes.string, + showPresets: PropTypes.bool, + ...ExposedDateInputProps, ...DateRange.propTypes, ...DefinedRange.propTypes, - className: PropTypes.string + className: PropTypes.string, }; export default DateRangePicker; diff --git a/src/components/DateRangePicker/index.scss b/src/components/DateRangePicker/index.scss index 1e2d18360..15e1c8c09 100644 --- a/src/components/DateRangePicker/index.scss +++ b/src/components/DateRangePicker/index.scss @@ -1,4 +1,27 @@ -.rdrDateRangePickerWrapper{ - display: inline-flex; +.rdrTitleAndInputWrapper{ + padding: 1rem; + background-color: #ffffff; + border-bottom: 1px solid #F2F2F2; + + display: flex; + padding: 1rem; + flex-direction: column; + align-items: flex-start; + gap: 1rem; + align-self: stretch; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +.rdrDefinedAndDateRangeWrapper{ + display: flex; user-select: none; + border-bottom: 1px solid #fff; + border-bottom-left-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; +} + +.rdrDateRangePickerWrapper{ + border-radius: 0.5rem; + border: 1px solid #fff; } \ No newline at end of file diff --git a/src/components/DayCell/index.js b/src/components/DayCell/index.js index 26f12539e..bfffa6c99 100644 --- a/src/components/DayCell/index.js +++ b/src/components/DayCell/index.js @@ -170,10 +170,9 @@ class DayCell extends Component { {this.renderSelectionPlaceholders()} {this.renderPreviewPlaceholder()} - { - dayContentRenderer?.(this.props.day) || + {dayContentRenderer?.(this.props.day) || ( {format(this.props.day, this.props.dayDisplayFormat)} - } + )} ); diff --git a/src/components/DayCell/index.scss b/src/components/DayCell/index.scss index e6f0c6543..c5b799304 100644 --- a/src/components/DayCell/index.scss +++ b/src/components/DayCell/index.scss @@ -10,7 +10,7 @@ display: block; position: relative; span{ - color: #1d2429; + color: #202020; } } diff --git a/src/components/DefinedRange/index.js b/src/components/DefinedRange/index.js index 7591154a8..04fe90b37 100644 --- a/src/components/DefinedRange/index.js +++ b/src/components/DefinedRange/index.js @@ -1,17 +1,20 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import styles from '../../styles'; -import { defaultInputRanges, defaultStaticRanges } from '../../defaultRanges'; +import { + // defaultInputRanges, + defaultStaticRanges, +} from '../../defaultRanges'; import { rangeShape } from '../DayCell'; import InputRangeField from '../InputRangeField'; import cx from 'classnames'; class DefinedRange extends Component { - constructor (props) { + constructor(props) { super(props); this.state = { rangeOffset: 0, - focusedInput: -1 + focusedInput: -1, }; } @@ -20,11 +23,11 @@ class DefinedRange extends Component { const selectedRange = ranges[focusedRange[0]]; if (!onChange || !selectedRange) return; onChange({ - [selectedRange.key || `range${focusedRange[0] + 1}`]: { ...selectedRange, ...range } + [selectedRange.key || `range${focusedRange[0] + 1}`]: { ...selectedRange, ...range }, }); }; - getRangeOptionValue (option) { + getRangeOptionValue(option) { const { ranges = [], focusedRange = [] } = this.props; if (typeof option.getCurrentValue !== 'function') { @@ -35,7 +38,7 @@ class DefinedRange extends Component { return option.getCurrentValue(selectedRange) || ''; } - getSelectedRange (ranges, staticRange) { + getSelectedRange(ranges, staticRange) { const focusedRangeIndex = ranges.findIndex(range => { if (!range.startDate || !range.endDate || range.disabled) return false; return staticRange.isSelected(range); @@ -44,7 +47,7 @@ class DefinedRange extends Component { return { selectedRange, focusedRangeIndex }; } - render () { + render() { const { headerContent, footerContent, @@ -54,7 +57,7 @@ class DefinedRange extends Component { ranges, renderStaticRangeLabel, rangeColors, - className + className, } = this.props; return ( @@ -73,21 +76,24 @@ class DefinedRange extends Component { return (