diff --git a/packages/react/src/components/DatePicker/index.js b/packages/react/src/components/DatePicker/index.js index 766467c59ac7..9d84a7bfcede 100644 --- a/packages/react/src/components/DatePicker/index.js +++ b/packages/react/src/components/DatePicker/index.js @@ -5,5 +5,13 @@ * LICENSE file in the root directory of this source tree. */ +import * as FeatureFlags from '@carbon/feature-flags'; +import { default as DatePickerNext } from './next/DatePicker'; +import { default as DatePickerClassic } from './DatePicker'; + +const DatePicker = FeatureFlags.enabled('enable-v11-release') + ? DatePickerNext + : DatePickerClassic; + +export default DatePicker; export { default as DatePickerSkeleton } from './DatePicker.Skeleton'; -export default from './DatePicker'; diff --git a/packages/react/src/components/DatePicker/next/DatePicker-test.js b/packages/react/src/components/DatePicker/next/DatePicker-test.js new file mode 100644 index 000000000000..58e615ef8d0e --- /dev/null +++ b/packages/react/src/components/DatePicker/next/DatePicker-test.js @@ -0,0 +1,228 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import DatePicker from './DatePicker'; +import { mount } from 'enzyme'; +import DatePickerInput from '../../DatePickerInput/next/DatePickerInput'; + +const prefix = 'bx'; + +function getFlatpickrCalendar() { + return document.querySelector(`.${prefix}--date-picker__calendar`); +} + +describe('DatePicker', () => { + describe('Renders as expected', () => { + let wrapper; + let datepicker; + + beforeEach(() => { + wrapper = mount( + {}} + className="extra-class" + dateFormat="m/d/Y"> + + + + ); + datepicker = wrapper.childAt(0); + }); + + it('should add extra classes that are passed via className', () => { + expect(datepicker.hasClass('extra-class')).toBe(true); + }); + + it('should add the date picker type as expected', () => { + expect(wrapper.props().datePickerType).toEqual(undefined); + wrapper.setProps({ datePickerType: 'simple' }); + expect(wrapper.props().datePickerType).toEqual('simple'); + }); + + it('should add the date format as expected', () => { + expect(wrapper.props().dateFormat).toEqual('m/d/Y'); + wrapper.setProps({ dateFormat: 'd/m/Y' }); + expect(wrapper.props().dateFormat).toEqual('d/m/Y'); + }); + + it('has the value as expected', () => { + expect(wrapper.props().value).toEqual(undefined); + wrapper.setProps({ value: '11/08/2017' }); + expect(wrapper.props().value).toEqual('11/08/2017'); + }); + + it('should render the children as expected', () => { + expect(wrapper.props().children.length).toEqual(2); + }); + }); + + describe('Simple date picker', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + it('has the value as expected', () => { + expect(wrapper.props().value).toEqual(undefined); + wrapper.setProps({ value: '11/08/2017' }); + expect(wrapper.props().value).toEqual('11/08/2017'); + }); + + it('should not initialize a calendar', () => { + expect(getFlatpickrCalendar()).not.toBeInTheDocument(); + }); + }); + + describe('Single date picker', () => { + let wrapper; + beforeEach(() => { + wrapper = mount( + {}} + datePickerType="single" + className="extra-class"> + + + ); + }); + + it('should initialize a calendar', () => { + expect(getFlatpickrCalendar()).toBeInTheDocument(); + }); + + it('should update the classnames', () => { + expect( + getFlatpickrCalendar().classList.contains( + `${prefix}--date-picker__calendar` + ) + ).toBe(true); + }); + + it('has the value as expected', () => { + expect(wrapper.props().value).toEqual(undefined); + wrapper.setProps({ value: '11/08/2017' }); + expect(wrapper.props().value).toEqual('11/08/2017'); + }); + }); + + describe('Date picker with locale', () => { + let wrapper; + let wrapperNoLocale; + beforeEach(() => { + wrapper = mount( + {}} + datePickerType="range" + className="extra-class" + locale="es"> + + + + ); + + wrapperNoLocale = mount( + {}} + datePickerType="range" + className="extra-class"> + + + + ); + }); + + it('has the range date picker locale', () => { + const datepicker = wrapper.find('DatePicker'); + expect(datepicker.props().locale).toBe('es'); + }); + + it('has the range date picker without locale defined', () => { + const datepicker = wrapperNoLocale.find('DatePicker'); + expect(datepicker.props().locale).toBe(undefined); + }); + }); + + describe('Date picker with minDate and maxDate', () => { + let mockConsoleError; + let wrapper; + + beforeEach(() => { + mockConsoleError = jest.spyOn(console, 'error'); + wrapper = mount( + {}} + datePickerType="range" + className="extra-class" + minDate="01/01/2018" + maxDate="01/30/2018"> + + + + ); + }); + + afterEach(() => { + mockConsoleError.mockRestore(); + }); + + it('has the range date picker with min and max dates', () => { + const datepicker = wrapper.find('DatePicker'); + expect(datepicker.props().minDate).toBe('01/01/2018'); + expect(datepicker.props().maxDate).toBe('01/30/2018'); + }); + + it('should not have "console.error" being created', () => { + expect(mockConsoleError).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/react/src/components/DatePicker/next/DatePicker.js b/packages/react/src/components/DatePicker/next/DatePicker.js new file mode 100644 index 000000000000..19aa34d3a133 --- /dev/null +++ b/packages/react/src/components/DatePicker/next/DatePicker.js @@ -0,0 +1,649 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; +import cx from 'classnames'; +import flatpickr from 'flatpickr'; +import l10n from 'flatpickr/dist/l10n/index'; +import DatePickerInput from '../../DatePickerInput'; +import carbonFlatpickrAppendToPlugin from '../plugins/appendToPlugin'; +import carbonFlatpickrFixEventsPlugin from '../plugins/fixEventsPlugin'; +import carbonFlatpickrRangePlugin from '../plugins/rangePlugin'; +import { match, keys } from '../../../internal/keyboard'; +import { usePrefix } from '../../../internal/usePrefix'; +import { useSavedCallback } from '../../../internal/useSavedCallback'; + +// Weekdays shorthand for english locale +l10n.en.weekdays.shorthand.forEach((_day, index) => { + const currentDay = l10n.en.weekdays.shorthand; + if (currentDay[index] === 'Thu' || currentDay[index] === 'Th') { + currentDay[index] = 'Th'; + } else { + currentDay[index] = currentDay[index].charAt(0); + } +}); + +const forEach = Array.prototype.forEach; + +/** + * @param {number} monthNumber The month number. + * @param {boolean} shorthand `true` to use shorthand month. + * @param {Locale} locale The Flatpickr locale data. + * @returns {string} The month string. + */ +const monthToStr = (monthNumber, shorthand, locale) => + locale.months[shorthand ? 'shorthand' : 'longhand'][monthNumber]; + +/** + * @param {object} config Plugin configuration. + * @param {boolean} [config.shorthand] `true` to use shorthand month. + * @param {string} config.selectorFlatpickrMonthYearContainer The CSS selector for the container of month/year selection UI. + * @param {string} config.selectorFlatpickrYearContainer The CSS selector for the container of year selection UI. + * @param {string} config.selectorFlatpickrCurrentMonth The CSS selector for the text-based month selection UI. + * @param {string} config.classFlatpickrCurrentMonth The CSS class for the text-based month selection UI. + * @returns {Plugin} A Flatpickr plugin to use text instead of ` + ) : ( + + ); + + return ( +
+ {labelText && ( + + )} +
+ {input} + +
+ {invalid && ( +
{invalidText}
+ )} + {warn &&
{warnText}
} + {helperText &&
{helperText}
} +
+ ); +}); + +DatePickerInput.propTypes = { + /** + * The type of the date picker: + * + * * `simple` - Without calendar dropdown. + * * `single` - With calendar dropdown and single date. + * * `range` - With calendar dropdown and a date range. + */ + datePickerType: PropTypes.oneOf(['simple', 'single', 'range']), + + /** + * Specify whether or not the input should be disabled + */ + disabled: PropTypes.bool, + + /** + * Provide text that is used alongside the control label for additional help + */ + helperText: PropTypes.node, + + /** + * Specify if the label should be hidden + */ + hideLabel: PropTypes.bool, + + /** + * The description of the calendar icon. + */ + iconDescription: deprecate( + PropTypes.string, + `\nThe prop \`iconDescriptionInput\` for DatePickerInput has been deprecated and is no longer used` + ), + + /** + * Specify an id that uniquely identifies the `` + */ + id: PropTypes.string.isRequired, + + /** + * Specify whether or not the input should be invalid + */ + invalid: PropTypes.bool, + + /** + * Specify the text to be rendered when the input is invalid + */ + invalidText: PropTypes.node, + + /** + * Provide the text that will be read by a screen reader when visiting this + * control + */ + labelText: PropTypes.node.isRequired, + + /** + * Specify an `onChange` handler that is called whenever a change in the + * input field has occurred + */ + onChange: PropTypes.func, + + /** + * Provide a function to be called when the input field is clicked + */ + onClick: PropTypes.func, + + /** + * Provide a function to be called when the input field is clicked + */ + openCalendar: deprecate( + PropTypes.func, + `\nThe prop \`openCalendar\` for DatePickerInput has been deprecated and is no longer used` + ), + + /** + * Provide a regular expression that the input value must match + */ + pattern: (props, propName, componentName) => { + if (props[propName] === undefined) { + return; + } + try { + new RegExp(props[propName]); + } catch (e) { + return new Error( + `Invalid value of prop '${propName}' supplied to '${componentName}', it should be a valid regular expression` + ); + } + }, + + /** + * Specify the placeholder text + */ + placeholder: PropTypes.string, + + /** + * Specify the size of the Date Picker Input. Currently supports either `sm`, 'md' (default) or 'lg` as an option. + * TODO V11: remove `xl` (replaced with lg) + */ + size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']), + + /** + * Specify the type of the `` + */ + type: PropTypes.string, + + /** + * Specify whether the control is currently in warning state + */ + warn: PropTypes.bool, + + /** + * Provide the text that is displayed when the control is in warning state + */ + warnText: PropTypes.node, +}; + +function DatePickerIcon({ datePickerType, invalid, warn, ...rest }) { + const prefix = usePrefix(); + + if (datePickerType === 'simple' && !invalid && !warn) { + return null; + } + + if (invalid) { + return ( + + ); + } + + if (!invalid && warn) { + return ( + + ); + } + + return ( + + ); +} + +DatePickerIcon.propTypes = { + /** + * The type of the date picker: + * + * * `simple` - Without calendar dropdown. + * * `single` - With calendar dropdown and single date. + * * `range` - With calendar dropdown and a date range. + */ + datePickerType: PropTypes.oneOf(['simple', 'single', 'range']), + + /** + * Specify whether or not the input should be invalid + */ + invalid: PropTypes.bool, + + /** + * Specify whether the control is currently in warning state + */ + warn: PropTypes.bool, +}; + +export default DatePickerInput;