Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
345 changes: 61 additions & 284 deletions src/components/DateInput.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<LineInput id={id} readOnly value={value} embedded={embedded} />
);
}

let calendarValue;

if (value) {
calendarValue = (typeof provisionalDate !== 'undefined') ? provisionalDate : date;
}

const className = (typeof provisionalDate !== 'undefined') ? styles.provisional : styles.normal;
id,
} = remainingProps;

return (
<DropdownInput
// eslint-disable-next-line react/jsx-props-no-spreading
{...remainingProps}
className={className}
focusPopup={this.focusCalendar}
open={open}
value={value}
api={this.handleDropdownInputApi}
onChange={this.handleDropdownInputChange}
onBeforeClose={this.handleDropdownInputBeforeClose}
onClose={this.handleDropdownInputClose}
onKeyDown={this.handleDropdownInputKeyDown}
onMount={this.handleDropdownInputMount}
onOpen={this.handleDropdownInputOpen}
>
<div ref={this.handleCalendarContainerRef}>
<Calendar
locale={locale}
// ref={this.handleCalendarRef}
value={calendarValue}
onChange={this.handleCalendarChange}
/>
</div>
</DropdownInput>
<LineInput
id={id}
asText={asText}
value={normalizedValue}
embedded={embedded}
readOnly={readOnly}
/>
);
}

return (
<input
// eslint-disable-next-line react/jsx-props-no-spreading
{...remainingProps}
type="date"
data-name={name}
className={className}
value={normalizedValue}
onChange={handleChange}

@spirosdi spirosdi Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The native date input (at least in Chrome) fires a change event per keystroke inside a segment. Typing the year 2025 over an existing 2025-12-01 produces "0002-12-01", "0020-12-01", "0202-12-01" and then "2025-12-01". Each one is committed. Clearing a segment commits "" as well. (As a side effect, the round trip through normalizeDateString shifts those (before 1000 year) years back a day, so the field visibly jumps around while typing.)

Could we use the changeable and committable enhancers instead, as the other text inputs do, so the value being edited is held locally and only committed on blur or enter? A BaseDateInput here can hold the native <input type="date">. This component can render committable(changeable(BaseDateInput)). handleChange and the getPath import can then go away.

/>
);
}

DateInput.propTypes = propTypes;
Expand Down
Loading
Loading