UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

257 lines (247 loc) 11.8 kB
import _objectWithoutProperties from '@babel/runtime/helpers/objectWithoutProperties'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import React__default, { useRef, useState } from 'react'; import styled from 'styled-components'; import { useLocale } from '@react-aria/i18n'; import { useDateSegment, useTimeField } from '@react-aria/datepicker'; import { useTimeFieldState } from '@react-stately/datepicker'; import '../BladeProvider/index.js'; import '../../tokens/global/index.js'; import '../Box/BaseBox/index.js'; import { BaseInput } from '../Input/BaseInput/BaseInput.js'; import '../Icons/index.js'; import '../../utils/assignWithoutSideEffects/index.js'; import { mergeRefs } from '../../utils/useMergeRefs.js'; import '../../utils/makeSpace/index.js'; import { jsxs, jsx } from 'react/jsx-runtime'; import { makeSpace } from '../../utils/makeSpace/makeSpace.js'; import { spacing } from '../../tokens/global/spacing.js'; import useTheme from '../BladeProvider/useTheme.js'; import { BaseBox } from '../Box/BaseBox/BaseBox.web.js'; import ClockIcon from '../Icons/ClockIcon/ClockIcon.js'; import { assignWithoutSideEffects } from '../../utils/assignWithoutSideEffects/assignWithoutSideEffects.js'; var _excluded = ["timeValue", "internalTimeValue", "onChange", "onTimeValueChange", "label", "helpText", "errorText", "successText", "validationState", "isDisabled", "isRequired", "necessityIndicator", "autoFocus", "name", "placeholder", "size", "labelPosition", "labelSuffix", "labelTrailing", "timeFormat", "testID", "accessibilityLabel", "inputRef", "referenceProps", "createCompleteTime", "setIsDropdownOpen"], _excluded2 = ["onKeyDown"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var StyledTimeSegment = /*#__PURE__*/styled.div.withConfig({ displayName: "TimeInputweb__StyledTimeSegment", componentId: "kpey4b-0" })(["padding-left:", ";padding-right:", ";box-sizing:content-box;font-variant-numeric:tabular-nums;text-align:right;outline:none;border-radius:", ";&:focus{background-color:", " !important;color:#ffffff !important;span{color:#000000 !important;}}"], makeSpace(spacing[1]), makeSpace(spacing[1]), makeSpace(spacing[1]), function (_ref) { var theme = _ref.theme; return theme.colors.interactive.background.primary.faded; }); /** * TimeSegment Component * * Renders individual time segments (hour, minute, AM/PM) or literal text (separators like ":", " "). * * For EDITABLE segments (hour, minute, dayPeriod): * - Uses React Aria's useDateSegment for full keyboard/accessibility support * - Renders as contentEditable div that users can click and type in * - Handles arrow keys, number input, focus management automatically * * For LITERAL segments (separators): * - Just renders static text in a span * - No interaction or focus behavior */ var TimeSegment = function TimeSegment(_ref2) { var segment = _ref2.segment, state = _ref2.state, isDisabled = _ref2.isDisabled; var ref = useRef(null); var _useState = useState(false), _useState2 = _slicedToArray(_useState, 2), isFocused = _useState2[0], setIsFocused = _useState2[1]; var _useTheme = useTheme(), theme = _useTheme.theme; var _useDateSegment = useDateSegment(segment, state, ref), segmentProps = _useDateSegment.segmentProps; // Filter out bi-directional text control characters (⁦⁩) that React Aria adds for RTL support but aren't needed for our UI - they appear as extra literal segments in some environments if (segment.type === 'literal' && segment.text === '⁦' || segment.text === '⁩') { return null; } // Calculate minWidth based on segment maxValue to prevent layout shifts var minWidth = segment.maxValue != null ? "".concat(String(segment.maxValue).length, "ch") : 'auto'; return /*#__PURE__*/jsxs(StyledTimeSegment, _objectSpread(_objectSpread({ ref: ref, theme: theme, style: _objectSpread(_objectSpread({}, segmentProps.style), {}, { minWidth: minWidth }), onFocus: function onFocus(e) { var _segmentProps$onFocus; setIsFocused(true); (_segmentProps$onFocus = segmentProps.onFocus) === null || _segmentProps$onFocus === void 0 || _segmentProps$onFocus.call(segmentProps, e); }, onBlur: function onBlur(e) { var _segmentProps$onBlur; setIsFocused(false); (_segmentProps$onBlur = segmentProps.onBlur) === null || _segmentProps$onBlur === void 0 || _segmentProps$onBlur.call(segmentProps, e); } }, segmentProps), {}, { children: [/*#__PURE__*/jsxs(BaseBox, { as: "span", display: "block", width: "100%", textAlign: "center", visibility: segment.isPlaceholder ? undefined : 'hidden', height: segment.isPlaceholder ? 'auto' : 'spacing.0', pointerEvents: "none", style: { color: isFocused ? theme.colors.surface.text.gray.normal : theme.colors.surface.text.gray.disabled }, children: [segment.placeholder, " "] }), /*#__PURE__*/jsx(BaseBox, { as: "span", style: { color: isDisabled ? theme.colors.surface.text.gray.disabled : theme.colors.surface.text.gray.normal }, children: segment.isPlaceholder ? '' : segment.type === 'dayPeriod' ? segment.text.toUpperCase() : segment.text })] })); }; /** * TimeInput Component * * A complete time input field built with React Aria for accessibility and keyboard interaction. * * ARCHITECTURE: * - Uses React Aria's useTimeFieldState for time value management & field-level behavior * - Uses Blade's BaseInput (as="div") for styling and form integration * - Renders multiple TimeSegment components for individual time parts * * USER INTERACTION: * - Click on any segment (hour, minute, AM/PM) to focus and edit * - Type numbers directly to change values * - Use arrow keys to increment/decrement values * - Tab between segments for navigation * - Full keyboard accessibility support */ var _TimeInput = function _TimeInput(_ref3, ref) { var timeValue = _ref3.timeValue, internalTimeValue = _ref3.internalTimeValue, onChange = _ref3.onChange, onTimeValueChange = _ref3.onTimeValueChange, label = _ref3.label, helpText = _ref3.helpText, errorText = _ref3.errorText, successText = _ref3.successText, validationState = _ref3.validationState, isDisabled = _ref3.isDisabled, isRequired = _ref3.isRequired, necessityIndicator = _ref3.necessityIndicator, autoFocus = _ref3.autoFocus, name = _ref3.name, placeholder = _ref3.placeholder, _ref3$size = _ref3.size, size = _ref3$size === void 0 ? 'medium' : _ref3$size, labelPosition = _ref3.labelPosition, labelSuffix = _ref3.labelSuffix, labelTrailing = _ref3.labelTrailing, timeFormat = _ref3.timeFormat, testID = _ref3.testID, accessibilityLabel = _ref3.accessibilityLabel, inputRef = _ref3.inputRef, referenceProps = _ref3.referenceProps, createCompleteTime = _ref3.createCompleteTime, setIsDropdownOpen = _ref3.setIsDropdownOpen, props = _objectWithoutProperties(_ref3, _excluded); var currentTimeFormat = timeFormat !== null && timeFormat !== void 0 ? timeFormat : '12h'; var _useLocale = useLocale(), locale = _useLocale.locale; var state = useTimeFieldState({ label: label, locale: locale, hourCycle: currentTimeFormat === '12h' ? 12 : 24, value: internalTimeValue, // Use TimeValue directly from hook onChange: onTimeValueChange, // Use TimeValue onChange directly isDisabled: isDisabled, shouldForceLeadingZeros: true // Force leading zeros (01, 02, 03...) }); var timeFieldRef = useRef(null); var _useTimeField = useTimeField({ label: label }, state, timeFieldRef), fieldProps = _useTimeField.fieldProps; // Extract onKeyDown from referenceProps to handle Enter key for dropdown opening var _ref4 = referenceProps || {}, referenceOnKeyDown = _ref4.onKeyDown, otherReferenceProps = _objectWithoutProperties(_ref4, _excluded2); // Handle Enter key to open dropdown while preserving React Aria's keyboard navigation var handleKeyDown = function handleKeyDown(event) { var _fieldProps$onKeyDown; if (event.key === 'Enter' && !isDisabled) { // Trigger dropdown opening (same as clicking the input) referenceOnKeyDown === null || referenceOnKeyDown === void 0 || referenceOnKeyDown(event); return; } // Let React Aria handle all other keys for segment navigation (_fieldProps$onKeyDown = fieldProps.onKeyDown) === null || _fieldProps$onKeyDown === void 0 || _fieldProps$onKeyDown.call(fieldProps, event); }; var handleInputClick = React__default.useCallback(function (_e) { if (isDisabled) return; setIsDropdownOpen === null || setIsDropdownOpen === void 0 || setIsDropdownOpen(true); }, [isDisabled, setIsDropdownOpen]); return /*#__PURE__*/jsx(BaseBox, _objectSpread(_objectSpread({}, fieldProps), {}, { className: "timepicker-input", onClick: handleInputClick, onKeyDown: handleKeyDown, ref: mergeRefs(timeFieldRef, ref), children: /*#__PURE__*/jsx(BaseInput, _objectSpread(_objectSpread(_objectSpread({ ref: inputRef, as: "div", id: "timepicker", label: label, helpText: helpText, errorText: errorText, successText: successText, validationState: validationState, isDisabled: isDisabled, isRequired: isRequired, necessityIndicator: necessityIndicator, autoFocus: autoFocus // eslint-disable-line jsx-a11y/no-autofocus , name: name, size: size, labelPosition: labelPosition, labelSuffix: labelSuffix, labelTrailing: labelTrailing, leadingIcon: ClockIcon, popupId: referenceProps === null || referenceProps === void 0 ? void 0 : referenceProps['aria-controls'], isPopupExpanded: referenceProps === null || referenceProps === void 0 ? void 0 : referenceProps['aria-expanded'], hasPopup: referenceProps === null || referenceProps === void 0 ? void 0 : referenceProps['aria-haspopup'], testID: testID, accessibilityLabel: accessibilityLabel || label || 'Time picker' }, props), otherReferenceProps), {}, { children: /*#__PURE__*/jsx(BaseBox, { display: "flex", children: state.segments.map(function (segment, i) { return ( /*#__PURE__*/ // Fix for React Aria contentEditable focus issue // Wrapping each segment in a div prevents unwanted focus when clicking outside // See: https://github.com/adobe/react-spectrum/issues/3164 jsx(BaseBox, { children: /*#__PURE__*/jsx(TimeSegment, { segment: segment, state: state, isDisabled: isDisabled }) }, i) ); }) }) })) })); }; var TimeInput = /*#__PURE__*/assignWithoutSideEffects(/*#__PURE__*/React__default.forwardRef(_TimeInput), { displayName: 'TimeInput', componentId: 'TimeInput' }); export { TimeInput }; //# sourceMappingURL=TimeInput.web.js.map