@razorpay/blade
Version:
The Design System that powers Razorpay
257 lines (247 loc) • 11.8 kB
JavaScript
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