react-fatless-form
Version:
A lightweight React form package designed for simplicity that simplifies form handling and validation without unnecessary complexity or bloat.
775 lines (749 loc) • 139 kB
JavaScript
'use strict';
var React = require('react');
var ReactDOM = require('react-dom');
class FeedbackManager {
constructor() {
this.listeners = new Set();
this.feedbacks = [];
this.timeouts = new Map();
this.addFeedback = (message, options = {}) => {
var _a;
const id = Date.now();
const feedback = {
id,
message,
type: options.type || "toast",
variant: options.variant || "info",
autoDismiss: (_a = options.autoDismiss) !== null && _a !== undefined ? _a : true,
duration: options.duration || 5000,
onClose: options.onClose,
isFadingOut: false,
};
this.feedbacks = [...this.feedbacks, feedback];
this.notifyListeners();
if (feedback.autoDismiss) {
const timeout = setTimeout(() => this.startFadeOut(id), feedback.duration);
this.timeouts.set(id, timeout);
}
};
this.startFadeOut = (id) => {
this.feedbacks = this.feedbacks.map((f) => f.id === id ? Object.assign(Object.assign({}, f), { isFadingOut: true }) : f);
this.notifyListeners();
const timeout = setTimeout(() => this.removeFeedback(id), 300);
this.timeouts.set(id, timeout);
};
this.removeFeedback = (id) => {
const feedback = this.feedbacks.find((f) => f.id === id);
if (feedback === null || feedback === undefined ? undefined : feedback.onClose)
feedback.onClose();
this.feedbacks = this.feedbacks.filter((f) => f.id !== id);
this.timeouts.delete(id);
this.notifyListeners();
};
this.notifyListeners = () => {
this.listeners.forEach((listener) => listener(this.feedbacks));
};
this.subscribe = (listener) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
}
}
/**
* FeedbackManager
*
* A class for managing feedback notifications, such as toasts or alerts, with support for auto-dismissal, customizable durations, and fade-out animations.
* This utility helps centralize feedback handling in applications, offering a subscription-based model for real-time updates.
*
* ### Features:
* - Supports feedback types: "toast" and "alert".
* - Four visual variants: "info", "success", "error", and "warning".
* - Auto-dismiss functionality with configurable durations.
* - Handles fade-out animations before removal.
* - Easy integration with UI through a FeedbackContainer component.
*
* ### Usage:
*
* #### Importing and Instantiating:
* ```typescript
* import { feedbackManager } from 'react-fatless-form';
* ```
*
* #### Adding Feedback:
* ```typescript
* feedbackManager.addFeedback("Operation successful!", {
* type: "toast",
* variant: "success",
* autoDismiss: true,
* duration: 5000,
* onClose: () => console.log("Feedback closed!"),
* });
* ```
*
* #### Subscribing to Feedback Updates:
* ```typescript
* const unsubscribe = feedbackManager.subscribe((feedbacks) => {
* console.log("Current feedbacks:", feedbacks);
* });
*
* // Unsubscribe when no longer needed
* unsubscribe();
* ```
*
* #### Mounting the FeedbackContainer:
* The `FeedbackContainer` component listens for updates to feedback notifications and renders them appropriately.
* Add it once to your application, typically in your app's root component.
*
* ```tsx
* import { FeedbackContainer } from 'react-fatless-form';
*
* function App() {
* return (
* <div>
* <YourMainContent />
* <FeedbackContainer />
* </div>
* );
* }
* ```
*
* `FeedbackContainer` uses `ReactDOM.createPortal` to render feedback notifications at the root of the `document.body`.
* Ensure it is included in your component tree to visualize feedback.
*
* #### Example Integration with UI:
* ```tsx
* import { feedbackManager } from 'react-fatless-form';
*
* const App = () => {
* const handleClick = () => {
* feedbackManager.addFeedback("This is a success message!", {
* type: "toast",
* variant: "success",
* duration: 3000,
* });
* };
*
* return (
* <div>
* <button onClick={handleClick}>Show Feedback</button>
* <FeedbackContainer />
* </div>
* );
* };
* ```
*
* ### API:
* #### Methods:
* - `addFeedback(message: string, options?: FeedbackOptions): void`
* - Adds a new feedback to the list.
* - `message` (string): The feedback message to display.
* - `options` (FeedbackOptions): Optional configurations.
* - `type` ("toast" | "alert"): Type of feedback (default: "toast").
* - `variant` ("info" | "success" | "error" | "warning"): Visual variant (default: "info").
* - `autoDismiss` (boolean): Whether feedback should dismiss automatically (default: true).
* - `duration` (number): Auto-dismiss duration in milliseconds (default: 5000ms).
* - `onClose` (function): Callback to execute on feedback removal.
*
* - `removeFeedback(id: number): void`
* - Removes feedback immediately and triggers its `onClose` callback if provided.
*
* - `subscribe(listener: (feedbacks: Feedback[]) => void): () => void`
* - Registers a listener for feedback updates.
* - Returns an unsubscribe function to stop listening.
*
* #### Internal Methods:
* - `startFadeOut(id: number): void`
* - Initiates the fade-out animation before removing feedback.
*
* - `notifyListeners(): void`
* - Notifies all registered listeners of feedback updates.
*
* ### Types:
* - `FeedbackVariant`:
* - `"info"`, `"success"`, `"error"`, `"warning"`
* - `Feedback`:
* - `id` (number): Unique identifier.
* - `message` (string): Feedback content.
* - `type` ("toast" | "alert"): Feedback type.
* - `variant` (FeedbackVariant): Feedback variant.
* - `autoDismiss` (boolean): Whether to auto-dismiss.
* - `duration` (number): Auto-dismiss duration in milliseconds.
* - `onClose` (function): Callback when feedback is closed.
* - `isFadingOut` (boolean): Whether the feedback is fading out.
* - `FeedbackOptions`:
* - Optional properties for `addFeedback` method.
*/
const feedbackManager = new FeedbackManager();
function useFeedback() {
const [feedbacks, setFeedbacks] = React.useState([]);
React.useEffect(() => {
const listener = (newFeedbacks) => setFeedbacks(newFeedbacks);
const unsubscribe = feedbackManager.subscribe(listener);
return () => {
unsubscribe();
};
}, []);
return feedbacks;
}
function styleInject(css, ref) {
if ( ref === undefined ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z$9 = ".Feedback-module_alert-container__RnydS,.Feedback-module_toast-container__yxUI6{animation:Feedback-module_fade-in__F-If5 .3s ease;display:flex;gap:5px;position:fixed;right:5px;z-index:9999}.Feedback-module_alert-container__RnydS{flex-direction:column;left:5px;top:5px}.Feedback-module_toast-container__yxUI6{bottom:5px;flex-direction:column-reverse}.Feedback-module_alert__OllrK,.Feedback-module_toast__3xnv4{align-items:center;border:1px solid #fff;border-radius:8px;display:flex;padding:15px 20px}.Feedback-module_toast__3xnv4{max-width:450px;min-width:350px}.Feedback-module_info__1ncyU{background:linear-gradient(180deg,#d1ecf1,#fff)}.Feedback-module_success__hQASX{background:linear-gradient(180deg,#d4edda,#fff)}.Feedback-module_warning__Ac-rP{background:linear-gradient(180deg,#fff3cd,#fff)}.Feedback-module_error__VTPq6{background:linear-gradient(180deg,#f8d7da,#fff)}.Feedback-module_icon-error__mna7F,.Feedback-module_icon-info__CDmBm,.Feedback-module_icon-success__FabnY,.Feedback-module_icon-warning__lXS-P{align-items:center;border-radius:50%;color:#fff;display:flex;height:20px;justify-content:center;width:20px}.Feedback-module_icon-info__CDmBm{background-color:#27bcfd}.Feedback-module_icon-success__FabnY{background-color:#00d27a}.Feedback-module_icon-warning__lXS-P{background-color:#f5803e}.Feedback-module_icon-error__mna7F{background-color:#e63757}.Feedback-module_icon-container__SW5oN{align-items:center;background-color:#f5f5f5;border:1px solid #fff;border-radius:50%;box-shadow:0 0 8px rgba(0,0,0,.1);display:flex;height:40px;justify-content:center;max-width:40px;width:40px;width:100%}.Feedback-module_message-container__7ahjX{flex:1;padding-left:20px}.Feedback-module_message-heading__6nKUc{font-size:1rem;font-weight:700}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-info__VOoGF{color:#27bcfd}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-success__PHY1e{color:#00d27a}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-warning__V36sc{color:#f5803e}.Feedback-module_message-heading__6nKUc.Feedback-module_heading-error__TT2iz{color:#e63757}.Feedback-module_message-body__6kaEv{color:#404040;font-size:.9rem}.Feedback-module_btn-close__397Jb{background:none;border:none;color:#333;cursor:pointer;font-size:16px;margin-left:auto;opacity:.7;position:relative;transition:opacity .2s}.Feedback-module_btn-close__397Jb:hover{opacity:1}@keyframes Feedback-module_fade-in__F-If5{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes Feedback-module_fade-out__1WY6R{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-10px)}}.Feedback-module_fade-out__1WY6R{animation:Feedback-module_fade-out__1WY6R .3s ease forwards}";
var styles$8 = {"alert-container":"Feedback-module_alert-container__RnydS","toast-container":"Feedback-module_toast-container__yxUI6","fade-in":"Feedback-module_fade-in__F-If5","alert":"Feedback-module_alert__OllrK","toast":"Feedback-module_toast__3xnv4","info":"Feedback-module_info__1ncyU","success":"Feedback-module_success__hQASX","warning":"Feedback-module_warning__Ac-rP","error":"Feedback-module_error__VTPq6","icon-info":"Feedback-module_icon-info__CDmBm","icon-success":"Feedback-module_icon-success__FabnY","icon-warning":"Feedback-module_icon-warning__lXS-P","icon-error":"Feedback-module_icon-error__mna7F","icon-container":"Feedback-module_icon-container__SW5oN","message-container":"Feedback-module_message-container__7ahjX","message-heading":"Feedback-module_message-heading__6nKUc","heading-info":"Feedback-module_heading-info__VOoGF","heading-success":"Feedback-module_heading-success__PHY1e","heading-warning":"Feedback-module_heading-warning__V36sc","heading-error":"Feedback-module_heading-error__TT2iz","message-body":"Feedback-module_message-body__6kaEv","btn-close":"Feedback-module_btn-close__397Jb","fade-out":"Feedback-module_fade-out__1WY6R"};
styleInject(css_248z$9);
const getIcon = (variant) => {
const icons = {
success: React.createElement("span", { className: styles$8.icon }, "\u2713"),
error: React.createElement("span", { className: styles$8.icon }, "\u2715"),
warning: React.createElement("span", { className: styles$8.icon }, "!"),
info: React.createElement("span", { className: styles$8.icon }, "\u2139"),
};
return icons[variant] || icons.info;
};
function FeedbackContainer() {
const feedbacks = useFeedback();
const alerts = feedbacks.filter(({ type }) => type === "alert");
const toasts = feedbacks.filter(({ type }) => type === "toast");
return ReactDOM.createPortal(React.createElement(React.Fragment, null,
React.createElement("div", { className: styles$8["alert-container"] }, alerts.map(({ id, variant, message, isFadingOut }) => (React.createElement("div", { key: id, className: `${styles$8.alert} ${styles$8[variant]} ${isFadingOut ? styles$8["fade-out"] : ""}` },
React.createElement("div", { className: styles$8["icon-container"] },
React.createElement("div", { className: styles$8[`icon-${variant}`] }, getIcon(variant))),
React.createElement("div", { className: styles$8["message-container"] },
React.createElement("p", { className: styles$8["message-heading"] }, variant.replace(/^\w/, c => c.toUpperCase())),
React.createElement("p", { className: styles$8["message-body"] }, message)),
React.createElement("button", { type: "button", className: styles$8["btn-close"], "aria-label": "Close", onClick: () => feedbackManager.startFadeOut(id) }, "\u2715"))))),
React.createElement("div", { className: styles$8["toast-container"] }, toasts.map(({ id, variant, message, isFadingOut }) => (React.createElement("div", { key: id, className: `${styles$8.toast} ${styles$8[variant]} ${isFadingOut ? styles$8["fade-out"] : ""}` },
React.createElement("div", { className: styles$8["icon-container"] },
React.createElement("div", { className: styles$8[`icon-${variant}`] }, getIcon(variant))),
React.createElement("div", { className: styles$8["message-container"] },
React.createElement("p", { className: `${styles$8["message-heading"]} ${styles$8[`heading-${variant}`]}` }, variant.replace(/^\w/, c => c.toUpperCase())),
React.createElement("p", { className: styles$8["message-body"] }, message)),
React.createElement("button", { type: "button", className: styles$8["btn-close"], "aria-label": "Close", onClick: () => feedbackManager.startFadeOut(id) }, "\u2715")))))), document.body);
}
const FormContext = React.createContext(null);
function FormProvider({ children, form }) {
return React.createElement(FormContext.Provider, { value: form }, children);
}
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
function useFormContext() {
const context = React.useContext(FormContext);
if (!context) {
throw new Error('useFormContext must be used within a FormProvider');
}
return context;
}
function useField(name) {
const form = useFormContext();
const value = form.values[name];
const error = form.errors[name];
const touched = form.touched[name];
const onFocus = () => {
if (!touched) {
form.setFieldTouched(name, true);
}
};
const onBlur = () => {
if (!touched) {
form.setFieldTouched(name, true);
}
if (error) {
form.setFieldError(name, error);
}
};
const onChange = React.useCallback((e) => {
if (error) {
form.setFieldError(name, '');
}
if (typeof e === 'boolean') {
form.setFieldValue(name, e);
}
else if (e instanceof Date) {
form.setFieldValue(name, e);
}
else if (typeof e === 'string' || typeof e === 'number') {
form.setFieldValue(name, e);
}
else if (Array.isArray(e)) {
const selectedValues = e.map((option) => {
if (typeof option === 'object' && option !== null && 'value' in option) {
return option.value;
}
return option;
});
// @ts-ignore
form.setFieldArrayValue(name, selectedValues);
}
else if (e && 'target' in e) {
const target = e.target;
if (target instanceof HTMLSelectElement) {
if (target.multiple) {
const selectedValues = Array.from(target.selectedOptions, (option) => option.value);
form.setFieldArrayValue(name, selectedValues);
}
else {
form.setFieldValue(name, target.value);
}
}
else if (target instanceof HTMLInputElement) {
if (target.type === 'checkbox') {
// Handle checkbox inputs
form.setFieldValue(name, target.checked);
}
else {
const value = e.target.value;
// If it's a valid number string, convert to number
const numeric = Number(value);
if (value === '') {
form.setFieldValue(name, null);
}
else if (!isNaN(numeric) && value.trim() !== '') {
form.setFieldValue(name, numeric);
}
else {
form.setFieldValue(name, value); // it's a string
}
}
}
else {
form.setFieldValue(name, target.value);
}
}
else if (e instanceof FileList) {
form.setFieldValue(name, e ? Array.from(e) : []);
}
else {
form.setFieldValue(name, e);
}
}, [error]);
return { touched, value, error, onFocus, onBlur, onChange };
}
var css_248z$8 = ".Checkbox-module_checkbox__lSeQj{accent-color:var(--checkbox-checked);appearance:auto;box-sizing:content-box;cursor:pointer;height:16px;padding:12px;width:16px}.Checkbox-module_checkboxLabel__Ci5a6{color:var(--checkbox-label-color,rgba(0,0,0,.87));cursor:pointer;font-family:Roboto,Helvetica,Arial,sans-serif;font-size:.875rem;line-height:1.43;margin-left:8px;user-select:none}.Checkbox-module_switch__WrR8X{display:inline-block;height:22px;position:relative;width:38px}.Checkbox-module_switch__WrR8X input{height:0;opacity:0;width:0}.Checkbox-module_slider__XDFK7{background-color:#ccc;bottom:0;cursor:pointer;left:0;position:absolute;right:0;top:0;transition:.4s}.Checkbox-module_slider__XDFK7:before{background-color:#fff;bottom:4px;content:\"\";height:14px;left:5px;position:absolute;transition:.4s;width:14px}input:checked+.Checkbox-module_slider__XDFK7{background-color:#0078d4}input:focus+.Checkbox-module_slider__XDFK7{box-shadow:0 0 1px #0078d4}input:checked+.Checkbox-module_slider__XDFK7:before{transform:translateX(14px)}.Checkbox-module_slider__XDFK7.Checkbox-module_round__wcXDv{border-radius:34px}.Checkbox-module_slider__XDFK7.Checkbox-module_round__wcXDv:before{border-radius:50%}";
var styles$7 = {"checkbox":"Checkbox-module_checkbox__lSeQj","checkboxLabel":"Checkbox-module_checkboxLabel__Ci5a6","switch":"Checkbox-module_switch__WrR8X","slider":"Checkbox-module_slider__XDFK7","round":"Checkbox-module_round__wcXDv"};
styleInject(css_248z$8);
/**
* A reusable Checkbox component for handling single and multiple checkbox inputs.
*
* This component supports two modes:
* 1. **Single Checkbox**: For a single boolean value, with optional slider styles.
* 2. **Multiple Checkboxes**: For selecting multiple values from a list of options.
*
* ## Props
*
* - **SingleCheckboxProps**:
* - `name` (string): The name attribute for the checkbox input.
* - `label` (string, optional): The label displayed next to the checkbox.
* - `value` (boolean): The current checked state of the checkbox.
* - `onChange` (function): A callback triggered when the checkbox state changes.
* - `slider` ('rectangular' | 'rounded', optional): If provided, renders the checkbox as a switch.
* - `rectangular`: Renders a rectangular switch.
* - `rounded`: Renders a rounded switch.
*
* - **MultipleCheckboxProps**:
* - `name` (string): The name attribute for the group of checkboxes.
* - `label` (string, optional): A label displayed above the group of checkboxes.
* - `value` (array of strings or numbers): The current selected values.
* - `onChange` (function): A callback triggered with the updated array of selected values.
* - `options` (array of SelectOption): The options to display as checkboxes, where each option has:
* - `label` (string): The display label for the checkbox.
* - `value` (string or number): The value associated with the checkbox.
*
* ## Behavior
*
* - **Single Checkbox**:
* - If `options` is not provided, it renders a single checkbox.
* - If `slider` is provided, the checkbox is styled as a switch.
* - **Multiple Checkboxes**:
* - If `options` is provided and has at least one item, it renders a list of checkboxes.
* - If `options` is empty, the component renders nothing.
*
* ## Examples
*
* ### Single Checkbox (Default)
*
* ```tsx
* <Checkbox
* name="acceptTerms"
* label="Accept Terms and Conditions"
* value={true}
* onChange={(checked) => console.log(checked)}
* />
* ```
*
* ### Single Checkbox (Slider)
*
* ```tsx
* <Checkbox
* name="darkMode"
* label="Enable Dark Mode"
* value={false}
* onChange={(checked) => console.log(checked)}
* slider="rounded"
* />
* ```
*
* ### Multiple Checkboxes
*
* ```tsx
* <Checkbox
* name="preferences"
* label="Choose Preferences"
* value={['option1']}
* onChange={(selected) => console.log(selected)}
* options={[
* { label: 'Option 1', value: 'option1' },
* { label: 'Option 2', value: 'option2' },
* ]}
* />
* ```
*
* @param {CheckboxInputType} props - The props to configure the checkbox component.
*
* @returns {JSX.Element | null} The rendered checkbox component.
*/
function Checkbox(props) {
const { name, label } = props;
// Single checkbox case
if (!('options' in props)) {
const { value, onChange, slider } = props;
const inputElement = (React.createElement("input", { id: name, name: name, type: "checkbox", checked: value, onChange: (e) => onChange(e.target.checked), className: styles$7.checkbox }));
const labelElement = label && (React.createElement("span", { className: styles$7.checkboxLabel }, label));
if (!slider) {
return (React.createElement("label", { htmlFor: name, style: { display: "flex", alignItems: "center" } },
inputElement,
labelElement));
}
const sliderClass = `${styles$7.slider} ${slider === 'rounded' ? styles$7.round : ''}`;
return (React.createElement("div", null,
React.createElement("label", { className: styles$7.switch, htmlFor: name },
inputElement,
React.createElement("span", { className: sliderClass })),
labelElement));
}
// Multiple checkboxes case
const { options, value = [], onChange } = props;
if (!options || options.length === 0) {
return null;
}
return (React.createElement("div", null,
label && React.createElement("label", { className: styles$7.checkboxLabel }, label),
React.createElement("div", null, options.map((option, index) => (React.createElement("label", { key: `${option.value}-${index}`, style: {
display: "flex",
alignItems: "center",
margin: "4px 0",
cursor: "pointer"
} },
React.createElement("input", { id: `${name}-${option.value}`, type: "checkbox", name: name, checked: value.includes(option.value), onChange: (e) => onChange(e.target.checked
? [...value, option.value]
: value.filter((v) => v !== option.value)), className: styles$7.checkbox }),
React.createElement("span", { className: styles$7.checkboxLabel }, option.label)))))));
}
/**
* Custom hook to calculate label width and dynamically set
* styles for masking the parent's border behind the label,
* with support for theme switching via `data-theme`.
*
* @param label - The label text (for triggering recomputation when changed).
* @returns [labelRef, dynamicStyles] - Ref to attach to <label>, and inline CSS vars for styling.
*/
function useLabelStyle(label) {
const labelRef = React.useRef(null);
// State for label width and computed parent styles
const [labelWidth, setLabelWidth] = React.useState(0);
const [parentBackground, setParentBackground] = React.useState("transparent");
const [labelColor, setLabelColor] = React.useState("");
/**
* Computes width of the label and finds background/text color
* from the nearest visible parent (used for masking effect).
* This is debounced via requestAnimationFrame.
*/
const computeStyles = () => {
requestAnimationFrame(() => {
if (!labelRef.current)
return;
// Measure label width using layout API
const rect = labelRef.current.getBoundingClientRect();
setLabelWidth(rect.width);
// Traverse parent elements to find background and text color
const findParentStyles = (element) => {
let bgColor = "transparent";
let textColor = "";
while (element) {
const styles = getComputedStyle(element);
if (bgColor === "transparent" &&
styles.backgroundColor !== "rgba(0, 0, 0, 0)" &&
styles.backgroundColor !== "transparent") {
bgColor = styles.backgroundColor;
}
if (!textColor && styles.color !== "rgba(0, 0, 0, 0)") {
textColor = styles.color;
}
// Break early if both are found
if (bgColor !== "transparent" && textColor)
break;
element = element.parentElement;
}
// Fallback to body styles if nothing usable was found
const bodyStyles = getComputedStyle(document.body);
return {
bgColor: bgColor === "transparent"
? bodyStyles.backgroundColor || "var(--label-default-bg, #fff)"
: bgColor,
textColor: textColor || bodyStyles.color || "var(--label-default-text, #808080)",
};
};
const parentElement = labelRef.current.parentElement;
const { bgColor, textColor } = findParentStyles(parentElement);
setParentBackground(bgColor);
setLabelColor(textColor);
});
};
React.useEffect(() => {
var _a;
computeStyles();
// Observe nearest ancestor with `data-theme` for changes
const rootWithTheme = (_a = labelRef.current) === null || _a === undefined ? undefined : _a.closest("[data-theme]");
if (rootWithTheme) {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === "data-theme") {
computeStyles(); // Recalculate styles on theme change
}
}
});
observer.observe(rootWithTheme, { attributes: true });
// Cleanup observer on unmount
return () => observer.disconnect();
}
// Explicitly return undefined if no observer is created
return undefined;
}, [label]); // Only recompute when label text or theme changes
/**
* Inline CSS variables used by the component to style
* label masking and dynamic color/background overrides.
*/
const dynamicStyles = {
"--label-width": `${labelWidth}px`,
"--label-offset": "10px",
"--mask-background": parentBackground,
"--label-color": labelColor,
};
return [labelRef, dynamicStyles];
}
var css_248z$7 = ".Global-module_inputWrapper__F9Q9V{align-items:center;background-color:inherit;border:1px solid var(--input-wrapper-border);border-radius:4px;color:var(--input-value);display:flex;height:56px;margin-bottom:10px;position:relative;transition:border-color .3s ease;width:100%}.Global-module_inputWrapper__F9Q9V:hover{border:1px solid var(--input-wrapper-border-hover)}.Global-module_inputWrapper__F9Q9V.Global-module_selectBoxOpen__ByTM6,.Global-module_inputWrapper__F9Q9V:not(.Global-module_inputWrapperError__L-xXY):focus-within{border-color:var(--label-focus-color)}.Global-module_inputWrapper__F9Q9V.Global-module_selectBoxOpen__ByTM6:not(.Global-module_inputWrapperError__L-xXY) .Global-module_label__Kvs6m,.Global-module_inputWrapper__F9Q9V:not(.Global-module_inputWrapperError__L-xXY):focus-within .Global-module_label__Kvs6m{color:var(--label-focus-color)}.Global-module_inputWrapper__F9Q9V:before{background-color:var(--mask-background);content:\"\";height:1px;left:var(--label-offset,10px);position:absolute;top:-1px;width:var(--label-width,50px);z-index:1}.Global-module_inputWrapper__F9Q9V.Global-module_inputWrapperError__L-xXY{border:1px solid #b80000}.Global-module_label__Kvs6m{background-color:transparent;color:var(--label-color,var(--label-default-text,grey));font-family:inherit;font-size:.875rem;margin-left:var(--label-offset,10px);padding:0 4px;position:absolute;top:-10px;transition:color .3s ease;z-index:2}.Global-module_rightIcon__mrj0Y{align-items:center;background:none;border:none;cursor:pointer;display:inline-flex;font-size:18px;justify-content:center;position:absolute;right:2px;top:50%;transform:translateY(-50%);z-index:2}.Global-module_rightIcon__mrj0Y span{font-size:1.5rem;line-height:1;transform-origin:center;transition:transform .3s ease-in-out}.Global-module_rightIcon__mrj0Y:hover span{transform:scale(1.2)}.Global-module_label__Kvs6m.Global-module_errorLabel__TLZP7{color:var(--label-error-color)!important}.Global-module_errorMessage__kNxOf{color:var(--label-error-color);font-size:.8rem;margin-top:1px}.Global-module_disabled__lyuoF{cursor:not-allowed;opacity:.7;pointer-events:none}:root{--dropdown-bg:#fff;--dropdown-text:#333;--dropdown-border:#ccc;--option-hover-bg:#f0f0f0;--option-hover-text:#333;--option-selected-bg:#e0e0e0;--option-selected-text:#333;--search-bg:#fff;--search-text:#333;--search-border:#ccc;--placeholder-color:#ccc;--dropdown-header-bg:#fff;--dropdown-header-text:#333;--dropdown-header-border:#ccc;--action-button-hover:#e0e0e0;--action-button-text:#999;--action-button-hover-text:#333;--clear-button-color:#999;--clear-button-hover:#333;--clear-button-active:#000;--calendar-bg:#fff;--calendar-text:#333;--calendar-border:#ccc;--calendar-header-bg:#f5f5f5;--calendar-day-bg:#f9f9f9;--calendar-day-hover:#e0e0e0;--calendar-day-selected:#0078d4;--calendar-day-disabled:#f5f5f5;--calendar-day-disabled-text:#999;--timepicker-bg:#fff;--timepicker-text:#333;--timepicker-border:#ccc;--timepicker-header-bg:#f5f5f5;--timepicker-option-hover:#e0e0e0;--timepicker-option-selected:#0078d4;--timepicker-range-bg:#f5f5f5;--dateinput-value:#333;--dateinput-text:#333;--dateinput-placeholder:#999;--dateinput-disabled:#888;--label-default-bg:#fff;--label-default-text:grey;--label-focus-color:#0078d4;--label-error-color:#d32f2f;--input-wrapper-border:#e0e0e0;--input-wrapper-border-hover:#61616161;--input-placeholder:#999;--input-value:#333;--input-disabled:#888;--checkbox-unchecked:rgba(0,0,0,.54);--checkbox-checked:#1976d2;--checkbox-label-color:rgba(0,0,0,.87)}[data-theme=dark]{--dropdown-bg:#1e1e1e;--dropdown-text:#f5f5f5;--dropdown-border:#616161;--dropdown-transition-duration:0.2s;--dropdown-transition-timing:ease-out;--dropdown-opacity:1;--option-hover-bg:#535353;--option-hover-text:#fff;--option-selected-bg:#616161;--option-selected-text:#fff;--search-bg:#1e1e1e;--search-text:#f5f5f5;--search-border:#616161;--placeholder-color:#aaa;--dropdown-header-bg:#1e1e1e;--dropdown-header-text:#f5f5f5;--dropdown-header-border:#616161;--action-button-hover:#757575;--action-button-text:#aaa;--action-button-hover-text:#fff;--clear-button-color:#aaa;--clear-button-hover:#fff;--clear-button-active:#fff;--calendar-bg:#1e1e1e;--calendar-text:#f5f5f5;--calendar-border:#616161;--calendar-header-bg:#535353;--calendar-day-bg:transparent;--calendar-day-hover:#535353;--calendar-day-selected:#0078d4;--calendar-day-disabled:#616161;--calendar-day-disabled-text:#757575;--timepicker-bg:#1e1e1e;--timepicker-text:#f5f5f5;--timepicker-border:#616161;--timepicker-header-bg:#535353;--timepicker-option-hover:#535353;--timepicker-option-selected:#0078d4;--timepicker-range-bg:#535353;--dateinput-value:#fff;--dateinput-text:#f5f5f5;--dateinput-placeholder:#aaa;--dateinput-disabled:#999;--label-default-bg:#1e1e1e;--label-default-text:#aaa;--label-focus-color:#3a96dd;--label-error-color:#ff6b6b;--input-wrapper-border:#616161;--input-wrapper-border-hover:#e0e0e0;--input-placeholder:#aaa;--input-value:#f5f5f5;--input-disabled:#999;--checkbox-unchecked:hsla(0,0%,100%,.7);--checkbox-checked:#90caf9;--checkbox-label-color:hsla(0,0%,100%,.87)}";
var globalStyles = {"inputWrapper":"Global-module_inputWrapper__F9Q9V","inputWrapperError":"Global-module_inputWrapperError__L-xXY","selectBoxOpen":"Global-module_selectBoxOpen__ByTM6","label":"Global-module_label__Kvs6m","rightIcon":"Global-module_rightIcon__mrj0Y","errorLabel":"Global-module_errorLabel__TLZP7","errorMessage":"Global-module_errorMessage__kNxOf","disabled":"Global-module_disabled__lyuoF"};
styleInject(css_248z$7);
var css_248z$6 = ".DateInput-module_dateInput__bYAdI{background-color:inherit;border:none;color:var(--input-value);cursor:pointer;font-family:inherit;font-size:16px;padding:8px;position:relative;width:100%;z-index:1}.DateInput-module_dateInput__bYAdI::placeholder{color:var(--input-placeholder);opacity:1}.DateInput-module_dateInput__bYAdI:disabled{color:var(--input-disabled);cursor:not-allowed}.DateInput-module_dateInput__bYAdI:focus{outline:none}.DateInput-module_dateInput__bYAdI:not(:disabled):hover{background-color:var(--input-hover-bg,rgba(0,0,0,.05))}time.DateInput-module_icon__xKmsO{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:#fff;border-radius:.6em;box-shadow:0 1px 0 #bdbdbd,0 2px 0 #fff,0 3px 0 #bdbdbd,0 4px 0 #fff,0 5px 0 #bdbdbd,0 0 0 1px #bdbdbd;display:block;font-size:1em;height:7em;margin:2em auto;overflow:hidden;position:relative;-webkit-transform:rotate(0deg) skewY(0deg);transform:rotate(0deg) skewY(0deg);-webkit-transform-origin:50% 10%;transform-origin:50% 10%;width:7em}.DateInput-module_timePicker__EpNUE{background:var(--timepicker-bg);border-top:1px solid var(--timepicker-border);border:1px solid var(--timepicker-border);border-radius:4px;box-shadow:0 4px 8px rgba(0,0,0,.1);display:flex;flex-direction:column;height:100%;left:100.125%;overflow-y:auto;position:absolute;scrollbar-color:#ccc transparent;scrollbar-width:thin;top:0;width:90px;z-index:10}.DateInput-module_timeHeader__z3e81{align-items:center;background:var(--timepicker-header-bg);border-bottom:1px solid #ccc;border-radius:4px 4px 0 0;color:var(--timepicker-text);display:flex;font-size:.7rem;justify-content:space-between;padding:8px;position:sticky;top:0;z-index:1}.DateInput-module_timeOption__ZJwGo{background:transparent;border:none;border-radius:4px;color:var(--timepicker-text);cursor:pointer;font-size:.8rem;padding:8px;text-align:center;transition:background-color .2s;width:100%}.DateInput-module_timeOption__ZJwGo:hover:not(.DateInput-module_timeOptionSelected__uAtnc):not(.DateInput-module_timeOptionDisabled__-ugRC){background:var(--timepicker-option-hover)}.DateInput-module_timeOptionSelected__uAtnc{background:var(--timepicker-option-selected);color:#fff}.DateInput-module_timeOptionDisabled__-ugRC{color:var(--calendar-day-disabled-text);cursor:not-allowed;display:none}.DateInput-module_timeOption__ZJwGo:active{background:#ddd}time.DateInput-module_icon__xKmsO *{display:block;font-size:1em;font-style:normal;font-weight:700;text-align:center;width:100%}time.DateInput-module_icon__xKmsO strong{background-color:#fd9f1b;border-bottom:1px dashed #f37302;box-shadow:0 2px 0 #fd9f1b;color:#fff;padding:.4em 0;position:absolute;top:0}time.DateInput-module_icon__xKmsO em{bottom:.3em;color:#fd9f1b;position:absolute}time.DateInput-module_icon__xKmsO span{color:#2f2f2f;font-size:2.8em;letter-spacing:-.05em;padding-top:.8em;width:100%}time.DateInput-module_icon__xKmsO:focus,time.DateInput-module_icon__xKmsO:hover{-webkit-animation:DateInput-module_swing__-zagu .6s ease-out;animation:DateInput-module_swing__-zagu .6s ease-out}.DateInput-module_calendarDropdown__RfMqi{background:var(--calendar-bg);border:1px solid var(--calendar-border);border-radius:4px;box-shadow:0 4px 8px rgba(0,0,0,.1);color:var(--calendar-text);left:0;opacity:0;padding:16px;position:absolute;transform:translateY(-10px);transition:opacity .2s,transform .2s;width:100%;z-index:10}.DateInput-module_calendarDropdown__RfMqi.DateInput-module_open__FnLHL{opacity:1;transform:translateY(0)}.DateInput-module_calendarHeader__WMjnb{align-items:center;background:var(--calendar-header-bg);border-radius:4px;display:flex;justify-content:space-between;margin-bottom:8px;padding:8px}.DateInput-module_calendarHeader__WMjnb select{background:var(--calendar-bg);border:1px solid var(--calendar-border);border-radius:4px;color:var(--calendar-text);padding:4px}.DateInput-module_daysOfWeek__KqX6i,.DateInput-module_days__0E6SG{display:grid;gap:4px;grid-template-columns:repeat(7,1fr)}.DateInput-module_dayLabel__-w6e8{color:var(--calendar-text);font-size:.8rem;font-weight:400;text-align:center}.DateInput-module_day__TMSXL{background:var(--calendar-day-bg);border:none;border-radius:4px;color:var(--calendar-text);cursor:pointer;padding:8px;text-align:center;transition:background-color .2s}.DateInput-module_day__TMSXL:hover:not(.DateInput-module_daySelected__HVDzA):not(.DateInput-module_dayDisabled__Pyxs4){background:var(--calendar-day-hover)}.DateInput-module_daySelected__HVDzA{background:var(--calendar-day-selected);color:#fff}.DateInput-module_dayDisabled__Pyxs4{background:var(--calendar-day-disabled);color:var(--calendar-day-disabled-text);cursor:not-allowed}.DateInput-module_calendarDropdown__RfMqi.DateInput-module_above__091EZ{bottom:calc(100% + 4px);top:auto}.DateInput-module_calendarDropdown__RfMqi.DateInput-module_below__Kk7bw{bottom:auto;top:calc(100% + 4px)}.DateInput-module_timeOptionsContainer__25UHR{border-radius:0 0 4px 4px;max-height:200px;overflow-y:auto}.DateInput-module_timeRange__5AJ6S{background-color:var(--timepicker-range-bg);color:var(--timepicker-text);font-size:.7rem;margin-bottom:1px;padding:4px 8px;text-align:center}@-webkit-keyframes DateInput-module_swing__-zagu{0%{-webkit-transform:rotate(0deg) skewY(0deg)}20%{-webkit-transform:rotate(12deg) skewY(4deg)}60%{-webkit-transform:rotate(-9deg) skewY(-3deg)}80%{-webkit-transform:rotate(6deg) skewY(-2deg)}to{-webkit-transform:rotate(0deg) skewY(0deg)}}@keyframes DateInput-module_swing__-zagu{0%{transform:rotate(0deg) skewY(0deg)}20%{transform:rotate(12deg) skewY(4deg)}60%{transform:rotate(-9deg) skewY(-3deg)}80%{transform:rotate(6deg) skewY(-2deg)}to{transform:rotate(0deg) skewY(0deg)}}";
var styles$6 = {"dateInput":"DateInput-module_dateInput__bYAdI","icon":"DateInput-module_icon__xKmsO","timePicker":"DateInput-module_timePicker__EpNUE","timeHeader":"DateInput-module_timeHeader__z3e81","timeOption":"DateInput-module_timeOption__ZJwGo","timeOptionSelected":"DateInput-module_timeOptionSelected__uAtnc","timeOptionDisabled":"DateInput-module_timeOptionDisabled__-ugRC","swing":"DateInput-module_swing__-zagu","calendarDropdown":"DateInput-module_calendarDropdown__RfMqi","open":"DateInput-module_open__FnLHL","calendarHeader":"DateInput-module_calendarHeader__WMjnb","daysOfWeek":"DateInput-module_daysOfWeek__KqX6i","days":"DateInput-module_days__0E6SG","dayLabel":"DateInput-module_dayLabel__-w6e8","day":"DateInput-module_day__TMSXL","daySelected":"DateInput-module_daySelected__HVDzA","dayDisabled":"DateInput-module_dayDisabled__Pyxs4","above":"DateInput-module_above__091EZ","below":"DateInput-module_below__Kk7bw","timeOptionsContainer":"DateInput-module_timeOptionsContainer__25UHR","timeRange":"DateInput-module_timeRange__5AJ6S"};
styleInject(css_248z$6);
/**
* A customizable `DateInput` component for selecting or entering dates in a user-friendly manner.
*
* This component includes a text input field and an optional dropdown calendar for selecting dates.
* It supports disabling specific dates through `minDate` and `maxDate` props, which allow developers
* to control the range of selectable dates. Users can interact with the component via text input
* (formatted based on dateFormat prop) or by selecting a date directly from the calendar.
*
* ## Features:
* - **Customizable Styling**: Accepts an optional `className` prop for styling the input field.
* - **Calendar Dropdown**: Provides an intuitive calendar interface for selecting dates.
* - **Date Validation**: Ensures that only valid dates are accepted, with error messages for invalid inputs.
* - **Disabled State**: Fully disables interactions when the `disabled` prop is `true`.
* - **Date Restrictions**: Use `minDate` and `maxDate` to enforce a selectable date range.
* - **Keyboard and Mouse Support**: Allows users to input dates manually or pick from the calendar dropdown.
* - **Time Picker (Optional)**: Includes an optional time picker for selecting time along with the date.
* - **Date Formatting**: Supports multiple date formats via the `dateFormat` prop.
* - **Minimum Time**: Restricts time selection when `timePicker` is enabled via `minTime` prop.
*
* ## Example Usage:
* ```tsx
* <DateInput
* value={new Date()} // Pre-select today's date
* onChange={(date) => console.log('Selected Date:', date)} // Handle date selection
* minDate={new Date()} // Restrict to no past dates
* maxDate={new Date(2080, 11, 31)} // Allow dates only up to Dec 31, 2080
* dateFormat="MMMM dd, yyyy" // Use long month name format
* timePicker={true} // Enable time selection
* minTime="09:00" // Restrict times to 9AM or later
* maxTime="16:00" // Restrict times to 4PM or earlier
* className="custom-date-input" // Add custom styling to the input field
* />
* ```
*
* ## Props:
* - `value?: Date`
* - The initial selected date for the input field. Defaults to `null`.
* - Example: `new Date(2023, 11, 25)` for December 25, 2023.
*
* - `onChange?: (value: Date) => void`
* - Callback function triggered when the selected date changes.
* - Receives the newly selected `Date` object as an argument.
* - Example: `(date) => console.log(date.toISOString())`.
*
* - `disabled?: boolean`
* - Whether the input and calendar dropdown should be disabled.
* - Defaults to `false`.
* - Example: `disabled={true}` disables all interactions.
*
* - `minDate?: Date`
* - The earliest date that can be selected.
* - Defaults to `undefined` (no restriction).
* - Example: `minDate={new Date()}` prevents selecting past dates.
*
* - `maxDate?: Date`
* - The latest date that can be selected.
* - Defaults to `undefined` (no restriction).
* - Example: `maxDate={new Date(2033, 11, 31)}` restricts selection to dates on or before December 31, 2033.
*
* - `timePicker?: boolean`
* - Whether to include a time picker along with the date input.
*
* - `minTime?: string`
* - The earliest time that can be selected (format: "HH:MM").
* - Only applicable when `timePicker` is true.
* - Example: "09:00" restricts selection to 9AM or later.
*
* - `maxTime?: string`
* - The latest time that can be selected (format: "HH:MM").
* - Only applicable when `timePicker` is true.
* - Example: "16:00" restricts selection to 4PM or earlier.
*
* - `dateFormat?: string`
* - The format to display dates in the input field.
* - Options: 'MM/dd/yyyy' | 'dd/MM/yyyy' | 'yyyy-MM-dd' | 'MMMM dd, yyyy' | 'LLL dd, yyyy'
* - Defaults to 'dd/MM/yyyy'.
*
* - `noWeekends?: boolean`
* - When true, disables selection of weekend dates (Saturday and Sunday)
* - Defaults to false
* - Example: `noWeekends={true}`
*
* - `rightIcon?: JSX.Element`
* - An optional icon or element to display on the right side of the input field.
*
* - `className?: string`
* - An optional CSS class to apply custom styles to the input field.
* - Example: `className="custom-date-input"`.
*/
function DateInput({ value, onChange = () => { }, disabled = false, minDate, maxDate, timePicker = false, minTime, maxTime, dateFormat = 'dd/MM/yyyy', noWeekends = false, className, style, placeholder, rightIcon, label = '', error = '', }) {
var _a, _b;
const [isCalendarOpen, setIsCalendarOpen] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState('');
const inputRef = React.useRef(null);
const [inputValue, setInputValue] = React.useState('');
const [currentMonth, setCurrentMonth] = React.useState((_a = value === null || value === undefined ? undefined : value.getMonth()) !== null && _a !== undefined ? _a : new Date().getMonth());
const [currentYear, setCurrentYear] = React.useState((_b = value === null || value === undefined ? undefined : value.getFullYear()) !== null && _b !== undefined ? _b : new Date().getFullYear());
const [labelRef, dynamicStyles] = useLabelStyle(label);
const [position, setPosition] = React.useState('below');
const dropdownRef = React.useRef(null);
const timeOptionRefs = React.useRef([]);
const getPlaceholder = () => {
if (placeholder)
return placeholder;
switch (dateFormat) {
case 'MM/dd/yyyy':
return 'MM/DD/YYYY';
case 'dd/MM/yyyy':
return 'DD/MM/YYYY';
case 'yyyy-MM-dd':
return 'YYYY-MM-DD';
case 'MMMM dd, yyyy':
return 'Month DD, YYYY';
case 'LLL dd, yyyy':
return 'Mon DD, YYYY';
default:
return 'DD/MM/YYYY';
}
};
// Format date based on dateFormat prop
const formatDate = (date) => {
if (!date)
return '';
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
const monthName = date.toLocaleString('default', { month: 'long' });
const monthShort = date.toLocaleString('default', { month: 'short' });
switch (dateFormat) {
case 'MM/dd/yyyy':
return `${month}/${day}/${year}`;
case 'dd/MM/yyyy':
return `${day}/${month}/${year}`;
case 'yyyy-MM-dd':
return `${year}-${month}-${day}`;
case 'MMMM dd, yyyy':
return `${monthName} ${day}, ${year}`;
case 'LLL dd, yyyy':
return `${monthShort} ${day}, ${year}`;
default:
return `${day}/${month}/${year}`;
}
};
// Calculate dropdown position
React.useEffect(() => {
if (!isCalendarOpen || !inputRef.current || !dropdownRef.current)
return;
const calculatePosition = () => {
const controlRect = inputRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - controlRect.bottom;
const dropdownHeight = dropdownRef.current.scrollHeight;
// Position above if there's not enough space below but enough above
if (spaceBelow < dropdownHeight && controlRect.top > dropdownHeight) {
setPosition('above');
}
else {
setPosition('below