From fac6134cef8b888789462d902a3be5c26c4729f0 Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Mon, 12 Oct 2026 13:44:28 -0600 Subject: [PATCH 1/7] Switch to functional component and use native date input --- src/components/DateInput.jsx | 345 +++++++---------------------------- 1 file changed, 61 insertions(+), 284 deletions(-) diff --git a/src/components/DateInput.jsx b/src/components/DateInput.jsx index c6f9d4e..edc9ecd 100644 --- a/src/components/DateInput.jsx +++ b/src/components/DateInput.jsx @@ -1,309 +1,86 @@ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import Calendar from 'react-calendar'; -import BaseDropdownInput from './DropdownInput'; import LineInput from './LineInput'; -import changeable from '../enhancers/changeable'; -import committable from '../enhancers/committable'; -import { getPath } from '../helpers/pathHelpers'; -import styles from '../../styles/cspace-input/DateInput.css'; -// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved -import '!style-loader!css-loader!../../styles/react-calendar/calendar.css'; - -import { - formatDate, - normalizeDateString, - normalizeISO8601DateString, - parseNormalizedDate, -} from '../helpers/dateHelpers'; - -const DropdownInput = committable(changeable(BaseDropdownInput)); +import { normalizeDateString } from '../helpers/dateHelpers'; +import { getPath, pathPropType } from '../helpers/pathHelpers'; +import styles from '../../styles/cspace-input/LineInput.css'; const propTypes = { - // TODO: Stop using propTypes in isInput, and in render method of cspace-ui Field component. - // Until then, propTypes need to be hoisted from the base component. - // eslint-disable-next-line react/forbid-foreign-prop-types - ...BaseDropdownInput.propTypes, + name: PropTypes.string, + parentPath: pathPropType, + subpath: pathPropType, + value: PropTypes.string, locale: PropTypes.string, - tooltip: PropTypes.string, onCommit: PropTypes.func, + asText: PropTypes.bool, readOnly: PropTypes.bool, + embedded: PropTypes.bool, }; const defaultProps = { + name: undefined, + parentPath: undefined, + subpath: undefined, + value: undefined, locale: 'en-US', - tooltip: '', onCommit: undefined, + asText: undefined, readOnly: undefined, + embedded: undefined, }; -export default class DateInput extends Component { - constructor(props) { - super(props); - - this.focusCalendar = this.focusCalendar.bind(this); - this.handleCalendarChange = this.handleCalendarChange.bind(this); - this.handleCalendarContainerRef = this.handleCalendarContainerRef.bind(this); - // this.handleCalendarRef = this.handleCalendarRef.bind(this); - this.handleDropdownInputApi = this.handleDropdownInputApi.bind(this); - this.handleDropdownInputBeforeClose = this.handleDropdownInputBeforeClose.bind(this); - this.handleDropdownInputChange = this.handleDropdownInputChange.bind(this); - this.handleDropdownInputClose = this.handleDropdownInputClose.bind(this); - this.handleDropdownInputKeyDown = this.handleDropdownInputKeyDown.bind(this); - this.handleDropdownInputMount = this.handleDropdownInputMount.bind(this); - this.handleDropdownInputOpen = this.handleDropdownInputOpen.bind(this); - - const value = normalizeISO8601DateString(props.value) || props.value; - - this.state = { - value, - date: parseNormalizedDate(value), - open: false, - }; - } - - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(nextProps) { - const nextValue = normalizeISO8601DateString(nextProps.value) || nextProps.value; - - this.setState({ - value: nextValue, - date: parseNormalizedDate(nextValue), - }); - } - - handleCalendarChange(date) { - this.setState({ - value: formatDate(date), - open: false, - }); - - this.commit(date); - this.focusInput(); - } - - // handleCalendarRef(ref) { - // this.calendar = ref; - // } - - handleCalendarContainerRef(ref) { - this.calendarContainerDomNode = ref; - } - - handleDropdownInputApi(api) { - this.dropdownInputApi = api; - } - - handleDropdownInputBeforeClose(isCancelled) { - if (isCancelled) { - const { - value, - } = this.props; - - this.setState({ - provisionalDate: undefined, - value: normalizeISO8601DateString(value), - }); +export default function DateInput(props) { + const { + value, + name, + onCommit, + asText, + embedded, + locale, + readOnly, + parentPath, + subpath, + ...remainingProps + } = props; + + const handleChange = (event) => { + const eventValue = event.target.value; + + const initialValue = normalizeDateString(value, locale) || ''; + if (onCommit && eventValue !== initialValue) { + onCommit(getPath(props), eventValue); } - } + }; - handleDropdownInputChange(value) { + const className = embedded ? styles.embedded : styles.normal; + const normalizedValue = normalizeDateString(value, locale) || ''; + if (asText || readOnly) { const { - locale, - } = this.props; - - const date = parseNormalizedDate(normalizeDateString(value, locale)); - - this.setState({ - value, - provisionalDate: date, - open: true, - }); - } - - handleDropdownInputClose() { - const nextState = { - provisionalDate: undefined, - open: false, - }; - - const { - provisionalDate, - value, - } = this.state; - - if (typeof provisionalDate !== 'undefined' && value === '') { - // Normally enter must be pressed on the provisional value in order to select a matching - // date, but make an exception for the blank value. This allows fields to be cleared - // without ever pressing enter. This is required by DRYD-227. - - nextState.value = ''; - - const { - onCommit, - } = this.props; - - if (onCommit) { - onCommit(getPath(this.props), ''); - } - } else { - const { - value: origValue, - } = this.props; - - nextState.value = normalizeISO8601DateString(origValue) || origValue; - } - - this.setState(nextState); - } - - handleDropdownInputKeyDown(event) { - const { - provisionalDate, - value, - } = this.state; - - if (event.key === 'Tab') { - // DRYD-264: Close the dropdown when tab is depressed. The calendar is still keyboard - // accessible by pressing down arrow. Need to close immediately (synchronously) -- otherwise - // the calendar will receive focus before closing, leaving nothing focused. To do this, the - // close() method of the dropdown needs to be called, rather than just passing open={false} - // as a prop. - - this.dropdownInputApi.close(); - - this.setState({ - open: false, - }); - } else if (typeof provisionalDate !== 'undefined' && event.key === 'Enter') { - event.preventDefault(); - - if (provisionalDate !== null || value === '') { - this.setState({ - open: false, - }); - - this.commit(provisionalDate); - } - } - } - - handleDropdownInputMount({ focusInput }) { - this.focusInput = focusInput; - } - - handleDropdownInputOpen() { - const { - open, - } = this.state; - - if (!open) { - this.setState({ - open: true, - provisionalDate: undefined, - }); - } - } - - commit(date) { - const { - onCommit, - } = this.props; - - const { - value: initialValue, - } = this.props; - - const normalizedInitialValue = normalizeISO8601DateString(initialValue); - const nextValue = formatDate(date); - - if ( - onCommit - && (nextValue || normalizedInitialValue) - && (nextValue !== normalizedInitialValue) - ) { - onCommit(getPath(this.props), nextValue); - } - } - - focusCalendar() { - // TODO: react-calendar v6 no longer has the focus method. Maybe it will come back? - - // if (this.calendar) { - // this.calendar.focus(); - // } - - // For now do a bit of a DOM hack to focus. - - if (this.calendarContainerDomNode) { - const button = this.calendarContainerDomNode.querySelector('button'); - - if (button) { - button.focus(); - } - } - } - - render() { - const { - locale, - readOnly, - ...remainingProps - } = this.props; - - const { - open, - date, - provisionalDate, - value, - } = this.state; - - if (readOnly) { - const { - embedded, - id, - } = remainingProps; - - return ( - - ); - } - - let calendarValue; - - if (value) { - calendarValue = (typeof provisionalDate !== 'undefined') ? provisionalDate : date; - } - - const className = (typeof provisionalDate !== 'undefined') ? styles.provisional : styles.normal; + id, + } = remainingProps; return ( - -
- -
-
+ ); } + + return ( + + ); } DateInput.propTypes = propTypes; From a653bb3c7cf1abab98f05416baf08f75557a7d1d Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Mon, 12 Oct 2026 16:50:33 -0600 Subject: [PATCH 2/7] Remove tests which no longer apply --- test/specs/components/DateInput.spec.jsx | 196 ----------------------- 1 file changed, 196 deletions(-) diff --git a/test/specs/components/DateInput.spec.jsx b/test/specs/components/DateInput.spec.jsx index 458e102..062ffad 100644 --- a/test/specs/components/DateInput.spec.jsx +++ b/test/specs/components/DateInput.spec.jsx @@ -3,7 +3,6 @@ import React from 'react'; import { Simulate } from 'react-dom/test-utils'; import DateInput from '../../../src/components/DateInput'; -import commitHandler from '../../helpers/commitHandler'; import createTestContainer from '../../helpers/createTestContainer'; import { render } from '../../helpers/renderHelpers'; import { isInput } from '../../../src/helpers/inputHelpers'; @@ -24,127 +23,6 @@ describe('DateInput', function suite() { isInput().should.equal(true); }); - it('should be closed initially', function test() { - const EnhancedDateInput = commitHandler(DateInput); - render(, this.container); - - expect(this.container.querySelector('.react-calendar')).to.equal(null); - }); - - it('should render with a tooltip when provided', function test() { - const tooltip = 'test'; - render(, this.container); - - const input = this.container.querySelector('input'); - expect(input.title).to.equal(tooltip); - }); - - it('should not open on focus', function test() { - render(, this.container); - - const input = this.container.querySelector('input'); - - Simulate.focus(input); - - expect(this.container.querySelector('.react-calendar')).to.equal(null); - }); - - it('should open on mouse down', function test() { - render(, this.container); - - const input = this.container.querySelector('input'); - - Simulate.mouseDown(input); - - this.container.querySelector('.react-calendar').should.not.equal(null); - }); - - it('should open when the input value changes', function test() { - render(, this.container); - - const input = this.container.querySelector('input'); - - Simulate.change(input); - - this.container.querySelector('.react-calendar').should.not.equal(null); - }); - - it('should close the calendar dropdown when tab is pressed in the input', function test() { - render( - , this.container, - ); - - const input = this.container.querySelector('input'); - - Simulate.focus(input); - - input.value = '1999-04-22'; - - Simulate.change(input); - - this.container.querySelector('.react-calendar').should.not.equal(null); - - Simulate.keyDown(input, { key: 'Tab' }); - - expect(this.container.querySelector('.react-calendar')).to.equal(null); - }); - - it('should call onCommit when enter is depressed in the dropdown input', function test() { - let committedPath = null; - let committedValue = null; - - const handleCommit = (path, value) => { - committedPath = path; - committedValue = value; - }; - - render( - , this.container, - ); - - const input = this.container.querySelector('input'); - - input.value = '1999-04-22'; - - Simulate.change(input); - Simulate.keyDown(input, { key: 'Enter' }); - - committedPath.should.deep.equal(['birthDate']); - committedValue.should.equal('1999-04-22'); - }); - - it('should not call onCommit when the dropdown input value is unchanged from the initial value', function test() { - let handlerCalled = false; - - const handleCommit = () => { - handlerCalled = true; - }; - - render( - , this.container, - ); - - const input = this.container.querySelector('input'); - - input.value = '2011-11-02'; - - Simulate.change(input); - Simulate.keyPress(input, { key: 'Enter' }); - - handlerCalled.should.equal(false); - }); - it('should call onCommit when a value is selected in the calendar', function test() { let committedPath = null; let committedValue = null; @@ -234,51 +112,6 @@ describe('DateInput', function suite() { }); }); - it('should not commit a blank value if the calendar closes due to escape being depressed', function test() { - let committedPath = null; - let committedValue = null; - - const handleCommit = (path, value) => { - committedPath = path; - committedValue = value; - }; - - render( - , this.container, - ); - - const input = this.container.querySelector('input'); - - input.value = ''; - - Simulate.change(input); - Simulate.keyDown(input, { key: 'Escape' }); - - return new Promise((resolve) => { - window.setTimeout(() => { - expect(committedPath).to.equal(null); - expect(committedValue).to.equal(null); - - resolve(); - }, 0); - }); - }); - - it('should label weekdays based on locale', function test() { - render(, this.container); - - const input = this.container.querySelector('input'); - - Simulate.keyDown(input, { key: 'ArrowDown' }); - - this.container.querySelector('.react-calendar__month-view__weekdays__weekday').textContent - .should.equal('lun'); - }); - it('should normalize the value', function test() { render( , this.container); - - const input = this.container.querySelector('input'); - - input.value.should.equal('01/01/1995'); - }); - - it('should display year/month only dates', function test() { - render(, this.container); - - const input = this.container.querySelector('input'); - - input.value.should.equal('04-1995'); - }); - - it('should display invalid dates and persist if the field is clicked', function test() { - render(, this.container); - - const input = this.container.querySelector('input'); - - input.value.should.equal('01/01/1995'); - Simulate.mouseDown(input); - this.container.querySelector('.react-calendar').should.not.equal(null); - - Simulate.keyDown(input, { key: 'Escape' }); - input.value.should.equal('01/01/1995'); - }); }); From 7aea7c852a4e10e1f5dd7202b140dd77f26e4bb1 Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Mon, 12 Oct 2026 16:50:55 -0600 Subject: [PATCH 3/7] Update tests --- test/specs/components/DateInput.spec.jsx | 63 +++++++++++++++++------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/test/specs/components/DateInput.spec.jsx b/test/specs/components/DateInput.spec.jsx index 062ffad..a325830 100644 --- a/test/specs/components/DateInput.spec.jsx +++ b/test/specs/components/DateInput.spec.jsx @@ -11,7 +11,7 @@ const { expect } = chai; chai.should(); -describe('DateInput', function suite() { +describe.only('DateInput', function suite() { // iOS tests on SauceLabs are slow. this.timeout(10000); @@ -42,17 +42,13 @@ describe('DateInput', function suite() { const input = this.container.querySelector('input'); - Simulate.keyDown(input, { key: 'ArrowDown' }); - - const dateButton = this.container.querySelector('button.react-calendar__month-view__days__day'); - - Simulate.click(dateButton); + Simulate.change(input, { target: { value: '2022-10-02' } }); committedPath.should.deep.equal(['birthDate']); committedValue.should.not.equal(null); }); - it('should not call onCommit when the selected calendar date is unchanged from the initial value', function test() { + it('should not call onCommit when the input value is unchanged from the initial value', function test() { let handlerCalled = false; const handleCommit = () => { @@ -69,16 +65,12 @@ describe('DateInput', function suite() { const input = this.container.querySelector('input'); - Simulate.keyDown(input, { key: 'ArrowDown' }); - - const calendar = this.container.querySelector('.react-calendar'); - - Simulate.keyDown(calendar, { key: 'Escape' }); + Simulate.change(input, { target: { value: '2011-11-02' } }); handlerCalled.should.equal(false); }); - it('should commit a blank value when the calendar closes, even if enter was not pressed', function test() { + it('should commit a blank value', function test() { let committedPath = null; let committedValue = null; @@ -96,16 +88,44 @@ describe('DateInput', function suite() { ); const input = this.container.querySelector('input'); + Simulate.change(input, { target: { value: '' } }); - input.value = ''; + return new Promise((resolve) => { + window.setTimeout(() => { + committedPath.should.deep.equal(['birthDate']); + committedValue.should.equal(''); + + resolve(); + }, 0); + }); + }); + + it('should not commit a blank value if the calendar closes due to escape being depressed', function test() { + let committedPath = null; + let committedValue = null; + + const handleCommit = (path, value) => { + committedPath = path; + committedValue = value; + }; + + render( + , this.container, + ); + + const input = this.container.querySelector('input'); Simulate.change(input); - Simulate.blur(input); + Simulate.keyDown(input, { key: 'Escape' }); return new Promise((resolve) => { window.setTimeout(() => { - committedPath.should.deep.equal(['birthDate']); - committedValue.should.equal(''); + expect(committedPath).to.equal(null); + expect(committedValue).to.equal(null); resolve(); }, 0); @@ -141,4 +161,13 @@ describe('DateInput', function suite() { input.disabled.should.equal(true); input.value.should.equal('2017-03-11'); }); + + it('should render a LineInput if asText is true', function test() { + render(, this.container); + + const div = this.container.firstElementChild; + + div.className.should.contain('cspace-input-LineInput--normal'); + div.textContent.should.equal('2017-03-11'); + }); }); From e6a33737f91c68029ab67ab6a09b53b827e5a293 Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Mon, 12 Oct 2026 16:51:46 -0600 Subject: [PATCH 4/7] Remove unused styles --- styles/cspace-input/DateInput.css | 25 ------- styles/react-calendar/calendar.css | 111 ----------------------------- 2 files changed, 136 deletions(-) delete mode 100644 styles/cspace-input/DateInput.css delete mode 100644 styles/react-calendar/calendar.css diff --git a/styles/cspace-input/DateInput.css b/styles/cspace-input/DateInput.css deleted file mode 100644 index 049841d..0000000 --- a/styles/cspace-input/DateInput.css +++ /dev/null @@ -1,25 +0,0 @@ -@value textMedium from '../colors.css'; -@value inputHeight from '../dimensions.css'; - -.common > input, .common > textarea { - padding-right: calc(inputHeight - 1px); - background-image: url(../../images/calDate.svg); - background-repeat: no-repeat; - background-position: right; -} - -.common > div { - min-width: auto; -} - -.normal { - composes: common; -} - -.provisional { - composes: common; -} - -.provisional input, .provisional textarea { - color: textMedium; -} diff --git a/styles/react-calendar/calendar.css b/styles/react-calendar/calendar.css deleted file mode 100644 index a64fe7a..0000000 --- a/styles/react-calendar/calendar.css +++ /dev/null @@ -1,111 +0,0 @@ -.react-calendar { - position: relative; - width: 300px; - max-width: 100%; - background: white; - font-family: 'Open Sans', Arial, sans-serif; - font-size: 14px; - font-weight: 400; - color: rgb(70, 70, 70); -} -.react-calendar, -.react-calendar *, -.react-calendar *:before, -.react-calendar *:after { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} -.react-calendar button { - margin: 0; - border: 0; - outline: none; - font-family: 'Open Sans', Arial, sans-serif; - font-size: 14px; - font-weight: 400; - color: rgb(70, 70, 70); -} -.react-calendar button::-moz-focus-inner { - border: 0; - padding: 0; -} -.react-calendar__navigation { - border-bottom: 1px solid rgb(220, 220, 220); -} -.react-calendar__navigation button { - min-width: 44px; - background: none; - padding: 8px 0; -} -.react-calendar__navigation button:not([disabled]):hover { - background-color: #F0F5FB; -} -.react-calendar__navigation button:not([disabled]):focus { - outline: 1px dotted black; - z-index: 1; -} -.react-calendar__month-view__weekdays { - text-align: center; - border-bottom: 1px solid rgb(220, 220, 220); -} -.react-calendar__month-view__weekdays__weekday { - padding: 8px 0; - font-size: 12px; - color: rgb(160, 160, 160); -} -.react-calendar__month-view__weekdays__weekday > abbr { - text-decoration: none; -} -.react-calendar__month-view__weekNumbers { - font-weight: bold; -} -.react-calendar__month-view__weekNumbers .react-calendar__tile { - display: flex; - align-items: center; - justify-content: center; - font-size: .75em; - padding: calc(1em) calc(0.66666667em); -} -button.react-calendar__month-view__days__day--neighboringMonth { - color: rgb(160, 160, 160); -} -.react-calendar__year-view .react-calendar__tile, -.react-calendar__decade-view .react-calendar__tile, -.react-calendar__century-view .react-calendar__tile { - padding: 8px 0; -} -.react-calendar__tile { - max-width: 100%; - text-align: center; - padding: 8px 0; - background: none; -} -button.react-calendar__tile[disabled] { - background-color: #f0f0f0; -} -button.react-calendar__tile:not([disabled]):hover { - background-color: #F0F5FB; -} -button.react-calendar__tile:not([disabled]):focus { - outline: 1px dotted black; - z-index: 1; -} -button.react-calendar__tile--hasActive { - outline: 2px solid rgb(70, 70, 70); - outline-offset: -1px; - z-index: 1; -} -button.react-calendar__tile--active { - outline: 2px solid rgb(70, 70, 70); - outline-offset: -1px; - z-index: 1; -} -button.react-calendar__tile--now > abbr { - border-radius: 3px; - padding: 2px 8px; - background-color: rgb(120, 120, 120); - color: white; -} -.react-calendar--selectRange .react-calendar__tile--hover { - background-color: #e6e6e6; -} From 277cb3cd73ff54b4439531521ec0df488c1f3109 Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Tue, 13 Oct 2026 15:32:19 -0600 Subject: [PATCH 5/7] Remove react-calendar from dependencies --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3e27061..1ac6231 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "moment": "^2.24.0", "prop-types": "^15.6.2", "react": "^16.14.0", - "react-calendar": "^3.4.0", "react-dom": "^16.14.0", "react-quill": "^1.3.1", "sugar-date": "^2.0.4" From 448a24c75b912a7b14ac07461e7a04abc2e3e4a6 Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Tue, 13 Oct 2026 16:58:45 -0600 Subject: [PATCH 6/7] Fix typo in test --- test/specs/components/DateInput.spec.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/specs/components/DateInput.spec.jsx b/test/specs/components/DateInput.spec.jsx index a325830..0160543 100644 --- a/test/specs/components/DateInput.spec.jsx +++ b/test/specs/components/DateInput.spec.jsx @@ -11,7 +11,7 @@ const { expect } = chai; chai.should(); -describe.only('DateInput', function suite() { +describe('DateInput', function suite() { // iOS tests on SauceLabs are slow. this.timeout(10000); From c9845097754f4c745695caa571fd8fb68a405298 Mon Sep 17 00:00:00 2001 From: Michael Ritter Date: Fri, 16 Oct 2026 14:27:47 -0600 Subject: [PATCH 7/7] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4e9a6..acdb4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2.3.1 * Render CustomCompoundInput, InputTable, and RepeatingInput as a div instead of a fieldset when they have no legend, resolving fieldset missing legend accessibility errors +* Update DateInput to use the native input `type="date"` ## 2.3.0