diff --git a/packages/react/src/components/TimePicker/TimePicker-story.js b/packages/react/src/components/TimePicker/TimePicker-story.js index 00df813364a5..d32f3b13251d 100644 --- a/packages/react/src/components/TimePicker/TimePicker-story.js +++ b/packages/react/src/components/TimePicker/TimePicker-story.js @@ -6,7 +6,6 @@ */ import React from 'react'; -import { action } from '@storybook/addon-actions'; import { withKnobs, @@ -49,9 +48,6 @@ const props = { ), maxLength: number('Maximum length (maxLength in )', 5), size: select('Field size (size)', sizes, undefined) || undefined, - onClick: action('onClick'), - onChange: action('onChange'), - onBlur: action('onBlur'), }), select: () => ({ disabled: boolean('Disabled (disabled in )', false), diff --git a/packages/react/src/components/TimePicker/index.js b/packages/react/src/components/TimePicker/index.js index f41baa0c4a41..f1fdaeb0e0e5 100644 --- a/packages/react/src/components/TimePicker/index.js +++ b/packages/react/src/components/TimePicker/index.js @@ -5,4 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -export default from './TimePicker'; +import * as FeatureFlags from '@carbon/feature-flags'; + +import { default as TimePickerNext } from './next/TimePicker'; + +import { default as TimePickerClassic } from './TimePicker'; + +const TimePicker = FeatureFlags.enabled('enable-v11-release') + ? TimePickerNext + : TimePickerClassic; + +export default TimePicker; diff --git a/packages/react/src/components/TimePicker/next/TimePicker-test.js b/packages/react/src/components/TimePicker/next/TimePicker-test.js new file mode 100644 index 000000000000..7aec125bb018 --- /dev/null +++ b/packages/react/src/components/TimePicker/next/TimePicker-test.js @@ -0,0 +1,68 @@ +/** + * 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 { default as TimePicker } from './TimePicker'; + +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +describe('TimePicker', () => { + describe('input', () => { + it('renders as expected', () => { + render(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('passes classNames as expected', () => { + render(); + expect(screen.getByRole('textbox')).toHaveClass('🚀'); + }); + + it('should set type as expected', () => { + render(); + expect(screen.getByRole('textbox')).toHaveAttribute('type', 'text'); + }); + + it('should set value as expected', () => { + render(); + expect(screen.getByRole('textbox')).toHaveAttribute('value', '🐶'); + }); + + it('should set disabled as expected', () => { + const onClick = jest.fn(); + render(); + fireEvent.click(screen.getByRole('textbox')); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should set placeholder as expected', () => { + render(); + expect(screen.getByPlaceholderText('🧸')).toBeInTheDocument(); + }); + }); + + describe('label', () => { + it('does not render a label by default', () => { + render(); + expect(screen.queryByLabelText('🐳')).not.toBeInTheDocument(); + }); + + it('renders a label as expected', () => { + render(); + expect(screen.getByLabelText('🐳')).toBeInTheDocument(); + }); + }); + + describe('events', () => { + it('should write text inside the textbox', () => { + render(); + userEvent.type(screen.getByRole('textbox'), '🧛'); + expect(screen.getByRole('textbox')).toHaveValue('🧛'); + }); + }); +}); diff --git a/packages/react/src/components/TimePicker/next/TimePicker.js b/packages/react/src/components/TimePicker/next/TimePicker.js new file mode 100644 index 000000000000..d5851b7ecd76 --- /dev/null +++ b/packages/react/src/components/TimePicker/next/TimePicker.js @@ -0,0 +1,229 @@ +/** + * 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 from 'react'; +import cx from 'classnames'; +import { usePrefix } from '../../../internal/usePrefix'; +import deprecate from '../../../prop-types/deprecate'; + +const TimePicker = React.forwardRef(function TimePicker( + { + children, + className, + disabled = false, + hideLabel, + id, + invalidText = 'Invalid time format.', + invalid = false, + labelText, + light = false, + maxLength = 5, + onChange = () => {}, + onClick = () => {}, + onBlur = () => {}, + pattern = '(1[012]|[1-9]):[0-5][0-9](\\s)?', + placeholder = 'hh:mm', + size, + type = 'text', + value, + ...rest + }, + ref +) { + const prefix = usePrefix(); + + const [isValue, setValue] = React.useState(value); + const [prevValue, setPrevValue] = React.useState(value); + + if (value !== prevValue) { + setValue(value); + setPrevValue(value); + } + + function handleOnClick(evt) { + if (!disabled) { + setValue(isValue); + onClick(evt); + } + } + + function handleOnChange(evt) { + if (!disabled) { + setValue(isValue); + onChange(evt); + } + } + + function handleOnBlur(evt) { + if (!disabled) { + setValue(isValue); + onBlur(evt); + } + } + + const timePickerInputClasses = cx( + `${prefix}--time-picker__input-field`, + `${prefix}--text-input`, + [className], + { + [`${prefix}--text-input--light`]: light, + } + ); + + const timePickerClasses = cx({ + [`${prefix}--time-picker`]: true, + [`${prefix}--time-picker--light`]: light, + [`${prefix}--time-picker--invalid`]: invalid, + [`${prefix}--time-picker--${size}`]: size, + [className]: className, + }); + + const labelClasses = cx(`${prefix}--label`, { + [`${prefix}--visually-hidden`]: hideLabel, + [`${prefix}--label--disabled`]: disabled, + }); + + const label = labelText ? ( + + ) : null; + + const error = invalid ? ( +
{invalidText}
+ ) : null; + + return ( +
+ {label} +
+
+ +
+ {children} +
+ {error} +
+ ); +}); + +TimePicker.propTypes = { + /** + * Pass in the children that will be rendered next to the form control + */ + children: PropTypes.node, + + /** + * Specify an optional className to be applied to the container node + */ + className: PropTypes.string, + + /** + * Specify whether the `` should be disabled + */ + disabled: PropTypes.bool, + + /** + * Specify whether you want the underlying label to be visually hidden + */ + hideLabel: PropTypes.bool, + + /** + * Specify a custom `id` for the `` + */ + id: PropTypes.string.isRequired, + + /** + * Specify whether the control is currently invalid + */ + invalid: PropTypes.bool, + + /** + * Provide the text that is displayed when the control is in an invalid state + */ + invalidText: PropTypes.node, + + /** + * Provide the text that will be read by a screen reader when visiting this + * control + */ + labelText: PropTypes.node, + + /** + * `true` to use the light version. TODO: V12 remove this. + */ + light: deprecate( + PropTypes.bool, + 'The `light` prop for `Tile` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.' + ), + + /** + * Specify the maximum length of the time string in `` + */ + maxLength: PropTypes.number, + + /** + * Optionally provide an `onBlur` handler that is called whenever the + * `` loses focus + */ + onBlur: PropTypes.func, + + /** + * Optionally provide an `onChange` handler that is called whenever `` + * is updated + */ + onChange: PropTypes.func, + + /** + * Optionally provide an `onClick` handler that is called whenever the + * `` is clicked + */ + onClick: PropTypes.func, + + /** + * Specify the regular expression working as the pattern of the time string in `` + */ + pattern: PropTypes.string, + + /** + * Specify the placeholder attribute for the `` + */ + placeholder: PropTypes.string, + + /** + * Specify the size of the Time Picker. Currently supports either `sm`, 'md' (default) or 'lg` as an option. + */ + size: PropTypes.oneOf(['sm', 'md', 'lg']), + + /** + * Specify the type of the `` + */ + type: PropTypes.string, + + /** + * Specify the value of the `` + */ + value: PropTypes.string, +}; + +export default TimePicker;