nexus-shared 1.1.20 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +399 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +170 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +239 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +254 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +291 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -59
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -57
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -49
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -161
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { type CSSProperties, type FocusEvent, type FormEvent, type MouseEvent, type ReactNode, type Ref } from "react";
|
|
2
|
+
import type { ChoiceDesign, ChoiceOption, OptionValue, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
|
|
3
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
4
|
+
/** Marks the inputs that are options of a choice field, so a "select all" box beside them is not counted. */
|
|
5
|
+
export declare const OPTION_ATTRIBUTE = "data-nx-option";
|
|
6
|
+
export interface ChoiceFieldOptions {
|
|
7
|
+
id?: string;
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
fieldName?: string;
|
|
10
|
+
error?: string | null;
|
|
11
|
+
validateOn?: ValidateOn;
|
|
12
|
+
/** Uses a text label as the field name in messages. Off for a single checkbox, whose label is usually a sentence. */
|
|
13
|
+
nameFromLabel?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ChoiceFieldRules {
|
|
16
|
+
/** The field's message for its option inputs, or `null` when valid. */
|
|
17
|
+
check: (inputs: HTMLInputElement[], name: string | undefined) => string | null;
|
|
18
|
+
/** Everything `check` depends on (rules, a controlled value). The field is checked again when one changes. */
|
|
19
|
+
checkKeys: readonly unknown[];
|
|
20
|
+
/** Writes what follows from the choices straight to the DOM (a count, limits, a select-all box), after every change and reset. */
|
|
21
|
+
sync?: (inputs: HTMLInputElement[]) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Behavior shared by checkboxes, switches, radio groups, and sliders (fields made of native inputs in a container): ids, validation messages, and form integration, with the
|
|
25
|
+
* same timing as a text box. A message shows once focus leaves a field that was changed, or on submit, and then follows
|
|
26
|
+
* every change. Validity is set on the first option with `setCustomValidity`, so a plain `<form>` blocks submit and
|
|
27
|
+
* focuses it. Clicking an option re-renders nothing unless its message changes.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useChoiceField<E extends HTMLElement>(options: ChoiceFieldOptions, rules: ChoiceFieldRules): {
|
|
30
|
+
fieldId: string;
|
|
31
|
+
messageId: string;
|
|
32
|
+
containerRef: import("react").RefObject<E | null>;
|
|
33
|
+
error: string | null;
|
|
34
|
+
inputs: () => HTMLInputElement[];
|
|
35
|
+
onChange: () => void;
|
|
36
|
+
onBlur: (event: FocusEvent<E>) => void;
|
|
37
|
+
onInvalid: (event: FormEvent<HTMLInputElement>) => void;
|
|
38
|
+
};
|
|
39
|
+
/** Stops a click from changing a read-only option. The value stays in the form. */
|
|
40
|
+
export declare function preventChange(event: MouseEvent<HTMLInputElement>): void;
|
|
41
|
+
/**
|
|
42
|
+
* Where a read-only choice shows its note: under the option whose input was clicked (a click on its label text
|
|
43
|
+
* reaches the input too), or pressed with Space or an arrow key, which browsers turn into a click.
|
|
44
|
+
*/
|
|
45
|
+
export declare function optionAnchor(target: Element): Element | null;
|
|
46
|
+
/** Hides a field's read-only note when focus leaves the field, not when it moves between its options. */
|
|
47
|
+
export declare function leavesField(event: FocusEvent<HTMLElement>): boolean;
|
|
48
|
+
/** Points a ref given by the caller at the same node as our own. */
|
|
49
|
+
export declare function assignRef<T>(ref: Ref<T> | undefined, node: T | null): void;
|
|
50
|
+
export interface ChoiceItemProps<T extends OptionValue> {
|
|
51
|
+
type: "checkbox" | "radio";
|
|
52
|
+
groupId: string;
|
|
53
|
+
name: string;
|
|
54
|
+
option: ChoiceOption<T>;
|
|
55
|
+
index: number;
|
|
56
|
+
size: Size;
|
|
57
|
+
/** Controlled state; `undefined` leaves the option uncontrolled with `defaultChecked`. */
|
|
58
|
+
checked?: boolean;
|
|
59
|
+
defaultChecked?: boolean;
|
|
60
|
+
invalid: boolean;
|
|
61
|
+
readOnly: boolean;
|
|
62
|
+
onInvalid: (event: FormEvent<HTMLInputElement>) => void;
|
|
63
|
+
}
|
|
64
|
+
/** One option of a group: a native input and its label, icon, and description, all inside one clickable label. */
|
|
65
|
+
export declare function ChoiceItem<T extends OptionValue>({ type, groupId, name, option, index, size, checked, defaultChecked, invalid, readOnly, onInvalid }: ChoiceItemProps<T>): import("react").JSX.Element;
|
|
66
|
+
/** The legend of a choice group, with the required mark and, when read-only, a lock. */
|
|
67
|
+
export declare function ChoiceLegend({ label, required, lock }: {
|
|
68
|
+
label: ReactNode;
|
|
69
|
+
required?: boolean;
|
|
70
|
+
lock?: ReactNode;
|
|
71
|
+
}): import("react").JSX.Element | null;
|
|
72
|
+
/** Props shared by `CheckBoxGroup` and `RadioGroup`. */
|
|
73
|
+
export interface ChoiceGroupProps<T extends OptionValue> {
|
|
74
|
+
options: readonly ChoiceOption<T>[];
|
|
75
|
+
/** Form field name. Default: a unique id. */
|
|
76
|
+
name?: string;
|
|
77
|
+
/** The group's legend. Without one, pass `aria-label` or `aria-labelledby`. */
|
|
78
|
+
label?: ReactNode;
|
|
79
|
+
/** Help text under the options. An error takes its place. */
|
|
80
|
+
hint?: ReactNode;
|
|
81
|
+
/** An error from outside, such as a server message. */
|
|
82
|
+
error?: string | null;
|
|
83
|
+
/** Default `md`. */
|
|
84
|
+
size?: Size;
|
|
85
|
+
/** `default`: a box or circle beside each label. `cards`: each option is a card. */
|
|
86
|
+
design?: ChoiceDesign;
|
|
87
|
+
/** `vertical` (default) stacks options, in `columns` when set; `horizontal` puts them in a wrapping row. */
|
|
88
|
+
orientation?: "vertical" | "horizontal";
|
|
89
|
+
/** Columns for vertical options. They drop to fewer columns when each would be narrower than 160px. */
|
|
90
|
+
columns?: number;
|
|
91
|
+
required?: boolean;
|
|
92
|
+
disabled?: boolean;
|
|
93
|
+
/** Options can be focused but not changed; the value is still sent with a form. A lock follows the label, and a click shows a note. */
|
|
94
|
+
readOnly?: boolean;
|
|
95
|
+
/** What a read-only group says when someone tries to change it. */
|
|
96
|
+
readOnlyMessage?: string;
|
|
97
|
+
/** Name used in messages. Default: the label, when it is text. */
|
|
98
|
+
fieldName?: string;
|
|
99
|
+
/** When the message shows. Default `blur` (when focus leaves the group); submitting a form always shows it. */
|
|
100
|
+
validateOn?: ValidateOn;
|
|
101
|
+
id?: string;
|
|
102
|
+
className?: string;
|
|
103
|
+
style?: CSSProperties;
|
|
104
|
+
"aria-label"?: string;
|
|
105
|
+
"aria-labelledby"?: string;
|
|
106
|
+
}
|
|
107
|
+
type GroupStyle = CSSProperties & {
|
|
108
|
+
"--nx-choices-columns"?: number;
|
|
109
|
+
};
|
|
110
|
+
/** Class names and inline style of a group's fieldset and options list. */
|
|
111
|
+
export declare function groupLayout(props: ChoiceGroupProps<OptionValue>): {
|
|
112
|
+
className: string;
|
|
113
|
+
optionsStyle: GroupStyle | undefined;
|
|
114
|
+
};
|
|
115
|
+
/** The hint or error line under a group. */
|
|
116
|
+
export declare function GroupFooter({ id, hint, error, aside }: {
|
|
117
|
+
id: string;
|
|
118
|
+
hint?: ReactNode;
|
|
119
|
+
error?: ReactNode;
|
|
120
|
+
aside?: ReactNode;
|
|
121
|
+
}): import("react").JSX.Element | null;
|
|
122
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { InlineIcon, iconAlertCircle } from "nexus-icons";
|
|
4
|
+
import { useEffect, useEffectEvent, useId, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { hasContent } from "./Field.js";
|
|
7
|
+
import { isFirstInvalid } from "./InputField.js";
|
|
8
|
+
/** Marks the inputs that are options of a choice field, so a "select all" box beside them is not counted. */
|
|
9
|
+
export const OPTION_ATTRIBUTE = "data-nx-option";
|
|
10
|
+
/**
|
|
11
|
+
* Behavior shared by checkboxes, switches, radio groups, and sliders (fields made of native inputs in a container): ids, validation messages, and form integration, with the
|
|
12
|
+
* same timing as a text box. A message shows once focus leaves a field that was changed, or on submit, and then follows
|
|
13
|
+
* every change. Validity is set on the first option with `setCustomValidity`, so a plain `<form>` blocks submit and
|
|
14
|
+
* focuses it. Clicking an option re-renders nothing unless its message changes.
|
|
15
|
+
*/
|
|
16
|
+
export function useChoiceField(options, rules) {
|
|
17
|
+
const { id, label, fieldName, error, validateOn = "blur", nameFromLabel = true } = options;
|
|
18
|
+
const generatedId = useId();
|
|
19
|
+
const fieldId = id ?? generatedId;
|
|
20
|
+
const messageId = `${fieldId}-message`;
|
|
21
|
+
const name = fieldName ?? (nameFromLabel && typeof label === "string" ? label : undefined);
|
|
22
|
+
const containerRef = useRef(null);
|
|
23
|
+
const touchedRef = useRef(false);
|
|
24
|
+
const editedRef = useRef(false);
|
|
25
|
+
const shownRef = useRef(null);
|
|
26
|
+
const [shownError, setShownError] = useState(null);
|
|
27
|
+
function inputs() {
|
|
28
|
+
return Array.from(containerRef.current?.querySelectorAll(`input[${OPTION_ATTRIBUTE}]`) ?? []);
|
|
29
|
+
}
|
|
30
|
+
function check() {
|
|
31
|
+
const list = inputs();
|
|
32
|
+
const message = rules.check(list, name);
|
|
33
|
+
// One option carries the message, the first that can take focus, so a form moves focus there.
|
|
34
|
+
const target = list.find(input => !input.disabled);
|
|
35
|
+
for (const input of list)
|
|
36
|
+
input.setCustomValidity(input === target ? (message ?? "") : "");
|
|
37
|
+
return message;
|
|
38
|
+
}
|
|
39
|
+
function show(message) {
|
|
40
|
+
if (shownRef.current === message)
|
|
41
|
+
return;
|
|
42
|
+
shownRef.current = message;
|
|
43
|
+
setShownError(message);
|
|
44
|
+
}
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
rules.sync?.(inputs());
|
|
47
|
+
const message = check();
|
|
48
|
+
if (touchedRef.current)
|
|
49
|
+
show(message);
|
|
50
|
+
}, [...rules.checkKeys, name]);
|
|
51
|
+
// A form reset restores the default choices in the DOM; bring the derived state, validity, and message back too.
|
|
52
|
+
const onFormReset = useEffectEvent(() => {
|
|
53
|
+
touchedRef.current = false;
|
|
54
|
+
editedRef.current = false;
|
|
55
|
+
// React fires onChange only when an input's state differs from the last state it saw, and a reset changes the DOM
|
|
56
|
+
// without telling it. Assigning the restored state through the property lets React see it, so checking the same
|
|
57
|
+
// box again after a reset still reports the change.
|
|
58
|
+
for (const input of inputs()) {
|
|
59
|
+
if (input.type === "checkbox" || input.type === "radio")
|
|
60
|
+
input.checked = input.checked;
|
|
61
|
+
else
|
|
62
|
+
input.value = input.value;
|
|
63
|
+
}
|
|
64
|
+
rules.sync?.(inputs());
|
|
65
|
+
check();
|
|
66
|
+
show(null);
|
|
67
|
+
});
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const form = inputs()[0]?.form;
|
|
70
|
+
if (!form)
|
|
71
|
+
return;
|
|
72
|
+
// The reset event fires before the browser restores the values.
|
|
73
|
+
const onReset = () => setTimeout(() => onFormReset());
|
|
74
|
+
form.addEventListener("reset", onReset);
|
|
75
|
+
return () => form.removeEventListener("reset", onReset);
|
|
76
|
+
}, []);
|
|
77
|
+
/** For the container's `onChange`: every option's change bubbles here. */
|
|
78
|
+
function onChange() {
|
|
79
|
+
editedRef.current = true;
|
|
80
|
+
if (validateOn === "change")
|
|
81
|
+
touchedRef.current = true;
|
|
82
|
+
rules.sync?.(inputs());
|
|
83
|
+
const message = check();
|
|
84
|
+
if (touchedRef.current)
|
|
85
|
+
show(message);
|
|
86
|
+
}
|
|
87
|
+
/** For the container's `onBlur`: moving between options of the same field does not count as leaving it. */
|
|
88
|
+
function onBlur(event) {
|
|
89
|
+
if (event.relatedTarget instanceof Node && containerRef.current?.contains(event.relatedTarget))
|
|
90
|
+
return;
|
|
91
|
+
if (validateOn !== "submit" && editedRef.current) {
|
|
92
|
+
touchedRef.current = true;
|
|
93
|
+
show(check());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** For each option's `onInvalid`, which does not bubble. */
|
|
97
|
+
function onInvalid(event) {
|
|
98
|
+
// Our message replaces the browser's bubble.
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
const input = event.currentTarget;
|
|
101
|
+
touchedRef.current = true;
|
|
102
|
+
show(check() ?? input.validationMessage);
|
|
103
|
+
if (isFirstInvalid(input))
|
|
104
|
+
input.focus();
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
fieldId,
|
|
108
|
+
messageId,
|
|
109
|
+
containerRef,
|
|
110
|
+
error: error || shownError,
|
|
111
|
+
inputs,
|
|
112
|
+
onChange,
|
|
113
|
+
onBlur,
|
|
114
|
+
onInvalid,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Stops a click from changing a read-only option. The value stays in the form. */
|
|
118
|
+
export function preventChange(event) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Where a read-only choice shows its note: under the option whose input was clicked (a click on its label text
|
|
123
|
+
* reaches the input too), or pressed with Space or an arrow key, which browsers turn into a click.
|
|
124
|
+
*/
|
|
125
|
+
export function optionAnchor(target) {
|
|
126
|
+
return target instanceof HTMLInputElement ? target.closest(".nx-choice") : null;
|
|
127
|
+
}
|
|
128
|
+
/** Hides a field's read-only note when focus leaves the field, not when it moves between its options. */
|
|
129
|
+
export function leavesField(event) {
|
|
130
|
+
return !(event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget));
|
|
131
|
+
}
|
|
132
|
+
/** Points a ref given by the caller at the same node as our own. */
|
|
133
|
+
export function assignRef(ref, node) {
|
|
134
|
+
if (typeof ref === "function")
|
|
135
|
+
ref(node);
|
|
136
|
+
else if (ref)
|
|
137
|
+
ref.current = node;
|
|
138
|
+
}
|
|
139
|
+
/** One option of a group: a native input and its label, icon, and description, all inside one clickable label. */
|
|
140
|
+
export function ChoiceItem({ type, groupId, name, option, index, size, checked, defaultChecked, invalid, readOnly, onInvalid }) {
|
|
141
|
+
const descriptionId = hasContent(option.description) ? `${groupId}-${index}-description` : undefined;
|
|
142
|
+
// With a description, the name comes from the label text alone; the description is read as a description.
|
|
143
|
+
const labelId = descriptionId ? `${groupId}-${index}-label` : undefined;
|
|
144
|
+
return (_jsxs("label", { className: cx("nx-choice", `nx-choice--${size}`), children: [_jsx("input", { id: `${groupId}-${index}`, type: type, name: name, value: String(option.value), [OPTION_ATTRIBUTE]: index, className: cx("nx-choice__control", `nx-choice__control--${type}`), checked: checked, defaultChecked: checked === undefined ? defaultChecked : undefined, disabled: option.disabled, "aria-invalid": invalid || undefined, "aria-labelledby": labelId, "aria-describedby": descriptionId, "aria-readonly": type === "checkbox" && readOnly ? true : undefined, onClick: readOnly ? preventChange : undefined, onInvalid: onInvalid,
|
|
145
|
+
// A controlled option changes through the group's onValueChange; this silences React's missing-onChange warning.
|
|
146
|
+
onChange: checked === undefined ? undefined : noop }), hasContent(option.icon) && (_jsx("span", { className: "nx-choice__icon", "aria-hidden": "true", children: option.icon })), _jsxs("span", { className: "nx-choice__text", children: [_jsx("span", { id: labelId, className: "nx-choice__label", children: option.label }), descriptionId && (_jsx("span", { id: descriptionId, className: "nx-choice__description", children: option.description }))] })] }));
|
|
147
|
+
}
|
|
148
|
+
function noop() { }
|
|
149
|
+
/** The legend of a choice group, with the required mark and, when read-only, a lock. */
|
|
150
|
+
export function ChoiceLegend({ label, required, lock }) {
|
|
151
|
+
if (!hasContent(label))
|
|
152
|
+
return null;
|
|
153
|
+
return (_jsxs("legend", { className: "nx-field__label nx-choices__legend", children: [label, required && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), lock] }));
|
|
154
|
+
}
|
|
155
|
+
/** Class names and inline style of a group's fieldset and options list. */
|
|
156
|
+
export function groupLayout(props) {
|
|
157
|
+
const { size = "md", design = "default", orientation = "vertical", columns } = props;
|
|
158
|
+
return {
|
|
159
|
+
className: cx("nx-choices", `nx-choices--${size}`, design === "cards" && "nx-choices--cards", orientation === "horizontal" && "nx-choices--horizontal", props.className),
|
|
160
|
+
optionsStyle: (columns && columns > 1 ? { "--nx-choices-columns": columns } : undefined),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** The hint or error line under a group. */
|
|
164
|
+
export function GroupFooter({ id, hint, error, aside }) {
|
|
165
|
+
const hasError = hasContent(error);
|
|
166
|
+
if (!hasError && !hasContent(hint) && !aside)
|
|
167
|
+
return null;
|
|
168
|
+
return (_jsxs("div", { className: "nx-field__footer", children: [(hasError || hasContent(hint)) && (_jsxs("p", { id: id, className: cx("nx-field__message", hasError && "nx-field__message--error"), children: [hasError && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: hasError ? error : hint })] })), aside && _jsx("span", { className: "nx-field__aside", children: aside })] }));
|
|
169
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ChangeEvent, type ComponentPropsWithRef, type ReactNode } from "react";
|
|
2
|
+
import type { CalendarType, DateInput, DateRules, DateScript } from "../../Interfaces/DateInterfaces.ts";
|
|
3
|
+
import type { InputDesign, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
|
|
4
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
5
|
+
import { type CalendarProps } from "./Calendar.tsx";
|
|
6
|
+
type NativeInputProps = Omit<ComponentPropsWithRef<"input">, "type" | "size" | "value" | "defaultValue" | "min" | "max" | "step" | "prefix" | "pattern" | "minLength" | "maxLength" | "children" | keyof DateRules>;
|
|
7
|
+
export interface DatePickerProps extends NativeInputProps, DateRules, Pick<CalendarProps, "weekStart" | "weekendDays" | "isDateDisabled" | "showOutsideDays"> {
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
/** Help text under the box. */
|
|
10
|
+
hint?: ReactNode;
|
|
11
|
+
/** An error from outside, such as a server message. Shown before the field's own message. */
|
|
12
|
+
error?: string | null;
|
|
13
|
+
/** Default `standard`. */
|
|
14
|
+
design?: InputDesign;
|
|
15
|
+
/** Default `md`. */
|
|
16
|
+
size?: Size;
|
|
17
|
+
/** Decorative icon at the start. The calendar button is always at the end. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** Text before the value inside the box. */
|
|
20
|
+
prefix?: ReactNode;
|
|
21
|
+
/** The calendar the box shows and picks in: `ad` or `bs`. Default: the user's setting. The value is an ISO date either way. */
|
|
22
|
+
calendar?: CalendarType;
|
|
23
|
+
/** How the date is written in the box: `YYYY-MM-DD`, `DD/MM/YYYY`, `MMM D, YYYY`, ... Default: the user's setting. */
|
|
24
|
+
format?: string;
|
|
25
|
+
/** Digits and names of Nepali dates. Default: the user's setting. */
|
|
26
|
+
script?: DateScript;
|
|
27
|
+
/** Shows a clear button while there is a value. Default true. */
|
|
28
|
+
clearable?: boolean;
|
|
29
|
+
/** Closes the calendar once a day is chosen. Default true. */
|
|
30
|
+
closeOnSelect?: boolean;
|
|
31
|
+
/** What a read-only field says when someone tries to change it. Default "Read-only: this date cannot be changed." */
|
|
32
|
+
readOnlyMessage?: string;
|
|
33
|
+
/** Name used in messages, e.g. "Check-in is required." Default: the label, when it is text. */
|
|
34
|
+
fieldName?: string;
|
|
35
|
+
/** When the field shows its own message. Default `blur`; submitting a form always shows it. */
|
|
36
|
+
validateOn?: ValidateOn;
|
|
37
|
+
/** The date as an ISO string, or `null` for empty. A `Date` or an ISO date-time is accepted and read as a day. */
|
|
38
|
+
value?: DateInput;
|
|
39
|
+
defaultValue?: DateInput;
|
|
40
|
+
/** After each change: the ISO date, or `null` when the box is empty or holds text that is not a date. */
|
|
41
|
+
onValueChange?: (value: string | null, event: ChangeEvent<HTMLInputElement>) => void;
|
|
42
|
+
/** Class of the outer field. `inputClassName` goes on the input. */
|
|
43
|
+
className?: string;
|
|
44
|
+
inputClassName?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A date input: type a date in the user's format, or pick one from the calendar that opens under the box. Shows the
|
|
48
|
+
* English (AD) or Nepali (BS) calendar from the user's settings, and always holds a Gregorian ISO date, which a form
|
|
49
|
+
* sends through a hidden input with the field's `name`. Uncontrolled by default, so typing re-renders nothing.
|
|
50
|
+
*
|
|
51
|
+
* @example <DatePicker name="checkIn" label="Check-in" required min={todayIsoDate()} />
|
|
52
|
+
*/
|
|
53
|
+
export declare function DatePicker(props: DatePickerProps): import("react").JSX.Element;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconCalendar, iconX, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { formatDate, parseDateText, toIsoDate, usesDevanagari } from "../../Helpers/DateHelpers.js";
|
|
7
|
+
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
8
|
+
import { DEFAULT_FIELD_NAME, validateDate } from "../../Helpers/ValidationHelpers.js";
|
|
9
|
+
import { Calendar } from "./Calendar.js";
|
|
10
|
+
import { Field, hasContent } from "./Field.js";
|
|
11
|
+
import { setInputValue, useInputField } from "./InputField.js";
|
|
12
|
+
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
13
|
+
const GAP = 4;
|
|
14
|
+
const MARGIN = 8;
|
|
15
|
+
/**
|
|
16
|
+
* A date input: type a date in the user's format, or pick one from the calendar that opens under the box. Shows the
|
|
17
|
+
* English (AD) or Nepali (BS) calendar from the user's settings, and always holds a Gregorian ISO date, which a form
|
|
18
|
+
* sends through a hidden input with the field's `name`. Uncontrolled by default, so typing re-renders nothing.
|
|
19
|
+
*
|
|
20
|
+
* @example <DatePicker name="checkIn" label="Check-in" required min={todayIsoDate()} />
|
|
21
|
+
*/
|
|
22
|
+
export function DatePicker(props) {
|
|
23
|
+
const { label, hint, error, design = "standard", size = "md", icon, prefix, clearable = true, closeOnSelect = true, fieldName, readOnlyMessage = "Read-only: this date cannot be changed.", validateOn, value, defaultValue, onValueChange, weekStart, weekendDays, isDateDisabled, showOutsideDays, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
|
|
24
|
+
const preferences = useDatePreferences();
|
|
25
|
+
const settings = {
|
|
26
|
+
calendar: props.calendar ?? preferences.calendar,
|
|
27
|
+
format: props.format ?? preferences.format,
|
|
28
|
+
script: props.script ?? preferences.script,
|
|
29
|
+
};
|
|
30
|
+
const editable = !disabled && !readOnly;
|
|
31
|
+
const initial = toIsoDate(value !== undefined ? value : defaultValue);
|
|
32
|
+
const text = (iso) => formatDate(iso, settings);
|
|
33
|
+
const valueRef = useRef(initial);
|
|
34
|
+
const editedRef = useRef(false);
|
|
35
|
+
const hiddenRef = useRef(null);
|
|
36
|
+
const panelRef = useRef(null);
|
|
37
|
+
const focusInPanelRef = useRef(false);
|
|
38
|
+
const focusCalendarRef = useRef(false);
|
|
39
|
+
const [open, setOpen] = useState(false);
|
|
40
|
+
// Re-renders the calendar's selection while it is open; typing with it closed re-renders nothing.
|
|
41
|
+
const [panelValue, setPanelValue] = useState(initial);
|
|
42
|
+
const rules = {
|
|
43
|
+
required,
|
|
44
|
+
min,
|
|
45
|
+
max,
|
|
46
|
+
validate: iso => (iso && isDateDisabled?.(iso) ? unavailableMessage(text(iso)) : validate?.(iso)),
|
|
47
|
+
};
|
|
48
|
+
const field = useInputField({
|
|
49
|
+
...props,
|
|
50
|
+
onChange: event => {
|
|
51
|
+
const input = event.currentTarget;
|
|
52
|
+
const iso = input.value.trim() ? parseDateText(input.value, settings) : null;
|
|
53
|
+
const changed = iso !== valueRef.current;
|
|
54
|
+
editedRef.current = true;
|
|
55
|
+
valueRef.current = iso;
|
|
56
|
+
sync(iso);
|
|
57
|
+
if (open)
|
|
58
|
+
setPanelValue(iso);
|
|
59
|
+
onChange?.(event);
|
|
60
|
+
if (changed)
|
|
61
|
+
onValueChange?.(iso, event);
|
|
62
|
+
},
|
|
63
|
+
}, {
|
|
64
|
+
// Text that is not a date is checked as itself, so the message says what to type.
|
|
65
|
+
check: (input, messageName) => validateDate(valueRef.current ?? (input.value.trim() ? input.value : null), rules, messageName, text),
|
|
66
|
+
checkKeys: [value, required, min, max, validate, isDateDisabled, settings.calendar, settings.format, settings.script],
|
|
67
|
+
onReset: input => {
|
|
68
|
+
// Back to the date the field started with, written in the current format. (A hidden input cannot be read
|
|
69
|
+
// for it: setting its value also sets its default, so a reset leaves it as it was.)
|
|
70
|
+
editedRef.current = false;
|
|
71
|
+
valueRef.current = initial;
|
|
72
|
+
input.value = text(initial);
|
|
73
|
+
sync(initial);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const messageName = fieldName ?? (typeof label === "string" ? label : DEFAULT_FIELD_NAME);
|
|
77
|
+
// A read-only box has no calendar: a lock, and a note when someone tries to open or change it, say why.
|
|
78
|
+
const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, { keys: ["ArrowDown"] });
|
|
79
|
+
function unavailableMessage(day) {
|
|
80
|
+
return `${messageName} cannot be ${day}.`;
|
|
81
|
+
}
|
|
82
|
+
/** Writes what depends on the value straight to the DOM: the hidden input a form sends. */
|
|
83
|
+
function sync(iso) {
|
|
84
|
+
if (hiddenRef.current)
|
|
85
|
+
hiddenRef.current.value = iso ?? "";
|
|
86
|
+
}
|
|
87
|
+
// A controlled value that differs from the box replaces its text. So does a default value that arrives after
|
|
88
|
+
// mount (from an API, say), as long as the user has not typed anything yet.
|
|
89
|
+
useLayoutEffect(() => {
|
|
90
|
+
const input = field.elementRef.current;
|
|
91
|
+
if (!input || (value === undefined && editedRef.current) || initial === valueRef.current)
|
|
92
|
+
return;
|
|
93
|
+
valueRef.current = initial;
|
|
94
|
+
input.value = text(initial);
|
|
95
|
+
sync(initial);
|
|
96
|
+
setPanelValue(initial);
|
|
97
|
+
}, [initial]);
|
|
98
|
+
// New settings (another calendar, format, or script) rewrite the same date.
|
|
99
|
+
useLayoutEffect(() => {
|
|
100
|
+
const input = field.elementRef.current;
|
|
101
|
+
if (input && valueRef.current)
|
|
102
|
+
input.value = text(valueRef.current);
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
}, [settings.calendar, settings.format, settings.script]);
|
|
105
|
+
// React sets a hidden input's default value again on every render, and for a hidden input that is its value.
|
|
106
|
+
// After each render, before paint, the hidden input takes the box's date back.
|
|
107
|
+
useLayoutEffect(() => {
|
|
108
|
+
sync(valueRef.current);
|
|
109
|
+
});
|
|
110
|
+
// The popover is a native one, placed under the box (above it when there is no room); it is shown once the calendar
|
|
111
|
+
// has rendered inside it.
|
|
112
|
+
useLayoutEffect(() => {
|
|
113
|
+
const panel = panelRef.current;
|
|
114
|
+
if (!open || !panel)
|
|
115
|
+
return;
|
|
116
|
+
if (!panel.matches(":popover-open"))
|
|
117
|
+
panel.showPopover();
|
|
118
|
+
place();
|
|
119
|
+
if (focusCalendarRef.current) {
|
|
120
|
+
focusCalendarRef.current = false;
|
|
121
|
+
panel.querySelector('[role="gridcell"][tabindex="0"]')?.focus();
|
|
122
|
+
}
|
|
123
|
+
// The panel is fixed, so it follows the box when the page scrolls or the window changes size.
|
|
124
|
+
let frame = 0;
|
|
125
|
+
const reposition = () => {
|
|
126
|
+
cancelAnimationFrame(frame);
|
|
127
|
+
frame = requestAnimationFrame(place);
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener("scroll", reposition, { capture: true, passive: true });
|
|
130
|
+
window.addEventListener("resize", reposition);
|
|
131
|
+
return () => {
|
|
132
|
+
cancelAnimationFrame(frame);
|
|
133
|
+
window.removeEventListener("scroll", reposition, { capture: true });
|
|
134
|
+
window.removeEventListener("resize", reposition);
|
|
135
|
+
};
|
|
136
|
+
}, [open]);
|
|
137
|
+
useEffect(() => () => panelRef.current?.hidePopover?.(), []);
|
|
138
|
+
function place() {
|
|
139
|
+
const panel = panelRef.current;
|
|
140
|
+
const box = field.elementRef.current?.closest(".nx-field__box");
|
|
141
|
+
if (!panel || !box)
|
|
142
|
+
return;
|
|
143
|
+
const anchor = box.getBoundingClientRect();
|
|
144
|
+
const { width, height } = panel.getBoundingClientRect();
|
|
145
|
+
const below = anchor.bottom + GAP;
|
|
146
|
+
const fitsBelow = below + height <= window.innerHeight - MARGIN;
|
|
147
|
+
const top = fitsBelow || anchor.top - GAP - height < MARGIN ? below : anchor.top - GAP - height;
|
|
148
|
+
const left = Math.max(MARGIN, Math.min(anchor.left, window.innerWidth - width - MARGIN));
|
|
149
|
+
panel.style.top = `${Math.round(top)}px`;
|
|
150
|
+
panel.style.left = `${Math.round(left)}px`;
|
|
151
|
+
}
|
|
152
|
+
function openPanel(focusCalendar) {
|
|
153
|
+
if (!editable)
|
|
154
|
+
return;
|
|
155
|
+
// The box is the control: it keeps focus while the calendar is open, so Esc and Enter reach it.
|
|
156
|
+
field.elementRef.current?.focus();
|
|
157
|
+
focusCalendarRef.current = focusCalendar;
|
|
158
|
+
setPanelValue(valueRef.current);
|
|
159
|
+
setOpen(true);
|
|
160
|
+
}
|
|
161
|
+
function closePanel() {
|
|
162
|
+
panelRef.current?.hidePopover();
|
|
163
|
+
setOpen(false);
|
|
164
|
+
}
|
|
165
|
+
// Whether the calendar holds focus is known only before it hides: hiding it moves focus to the body.
|
|
166
|
+
function onBeforeToggle(event) {
|
|
167
|
+
if (event.newState === "closed")
|
|
168
|
+
focusInPanelRef.current = event.currentTarget.contains(document.activeElement);
|
|
169
|
+
}
|
|
170
|
+
function onToggle(event) {
|
|
171
|
+
if (event.newState !== "closed")
|
|
172
|
+
return;
|
|
173
|
+
setOpen(false);
|
|
174
|
+
// Focus returns to the box when the calendar had it.
|
|
175
|
+
if (focusInPanelRef.current) {
|
|
176
|
+
focusInPanelRef.current = false;
|
|
177
|
+
field.elementRef.current?.focus();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function select(iso) {
|
|
181
|
+
const input = field.elementRef.current;
|
|
182
|
+
if (!input)
|
|
183
|
+
return;
|
|
184
|
+
setInputValue(input, text(iso));
|
|
185
|
+
field.touch();
|
|
186
|
+
if (closeOnSelect)
|
|
187
|
+
closePanel();
|
|
188
|
+
}
|
|
189
|
+
function clear() {
|
|
190
|
+
const input = field.elementRef.current;
|
|
191
|
+
if (!input)
|
|
192
|
+
return;
|
|
193
|
+
setInputValue(input, "");
|
|
194
|
+
input.focus();
|
|
195
|
+
}
|
|
196
|
+
function handleKeyDown(event) {
|
|
197
|
+
const { key } = event;
|
|
198
|
+
readOnlyNotice.onKeyDown(event);
|
|
199
|
+
if (editable && key === "ArrowDown") {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
openPanel(true);
|
|
202
|
+
}
|
|
203
|
+
else if (key === "Escape" && open) {
|
|
204
|
+
// The popover closes itself; the page (a dialog, say) does not see this Esc.
|
|
205
|
+
event.stopPropagation();
|
|
206
|
+
}
|
|
207
|
+
else if (clearable && editable && key === "Escape" && event.currentTarget.value) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
event.stopPropagation();
|
|
210
|
+
clear();
|
|
211
|
+
}
|
|
212
|
+
else if (key === "Enter" && open) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
closePanel();
|
|
215
|
+
}
|
|
216
|
+
else if (key === "Tab" && open) {
|
|
217
|
+
// Tab moves on to the next field, not into the calendar; ArrowDown is the way in.
|
|
218
|
+
closePanel();
|
|
219
|
+
}
|
|
220
|
+
onKeyDown?.(event);
|
|
221
|
+
}
|
|
222
|
+
function handleClick(event) {
|
|
223
|
+
if (!open)
|
|
224
|
+
openPanel(false);
|
|
225
|
+
readOnlyNotice.onClick(event);
|
|
226
|
+
onClick?.(event);
|
|
227
|
+
}
|
|
228
|
+
function handleBlur(event) {
|
|
229
|
+
// Typed text takes the canonical form once it is a date.
|
|
230
|
+
const input = event.currentTarget;
|
|
231
|
+
if (valueRef.current && input.value !== text(valueRef.current))
|
|
232
|
+
input.value = text(valueRef.current);
|
|
233
|
+
// Focus moving anywhere but the calendar (its month and year menus) closes it.
|
|
234
|
+
if (open && !(event.relatedTarget instanceof Node && panelRef.current?.contains(event.relatedTarget)))
|
|
235
|
+
closePanel();
|
|
236
|
+
readOnlyNotice.hide();
|
|
237
|
+
field.controlProps.onBlur(event);
|
|
238
|
+
}
|
|
239
|
+
const showClear = clearable && editable;
|
|
240
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, design: design, size: size, hint: hint, error: field.error, icon: icon, prefix: prefix, actions: _jsxs(_Fragment, { children: [showClear && (_jsx("button", { type: "button", className: "nx-field__action nx-field__action--clear", tabIndex: -1, "aria-label": "Clear", title: "Clear", onMouseDown: event => event.preventDefault(), onClick: clear, children: _jsx(InlineIcon, { icon: iconX }) })), readOnlyNotice.lock, !readOnly && (_jsx("button", { type: "button", className: "nx-field__action nx-field__action--calendar", tabIndex: -1, "aria-label": "Open calendar", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? `${field.controlId}-calendar` : undefined, title: "Open calendar", disabled: disabled, onMouseDown: event => event.preventDefault(), onClick: () => (open ? closePanel() : openPanel(false)), children: _jsx(InlineIcon, { icon: iconCalendar }) }))] }), disabled: disabled, readOnly: readOnly, raiseLabel: Boolean(placeholder) || hasContent(prefix), className: cx("nx-field--date", className), style: style, children: [_jsx("input", { autoComplete: "off", spellCheck: false, ...inputProps, ...field.controlProps, type: "text", name: undefined, form: form, lang: usesDevanagari(settings.calendar, settings.script) ? "ne" : undefined, defaultValue: text(initial), className: cx("nx-field__control", "nx-field__control--date", inputClassName), placeholder: placeholder || " ", disabled: disabled, readOnly: readOnly, onBlur: handleBlur, onFocus: onFocus, onKeyDown: handleKeyDown, onClick: handleClick }), name && _jsx("input", { ref: hiddenRef, type: "hidden", name: name, form: form, disabled: disabled, defaultValue: initial ?? "" })] }), readOnlyNotice.notice, !readOnly && (_jsx("div", { ref: panelRef, id: `${field.controlId}-calendar`, popover: "auto", role: "dialog", "aria-label": "Calendar", className: "nx-popover nx-date-popover", onBeforeToggle: onBeforeToggle, onToggle: onToggle,
|
|
241
|
+
// The box keeps focus while days and buttons are clicked; the month and year menus need focus to open.
|
|
242
|
+
onMouseDown: event => {
|
|
243
|
+
if (!event.target.closest("select"))
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
}, children: open && (_jsx(Calendar, { size: "sm", calendar: settings.calendar, script: settings.script, value: panelValue, onSelect: select, onValueChange: iso => iso === null && clear(), min: min, max: max, isDateDisabled: isDateDisabled, weekStart: weekStart, weekendDays: weekendDays, showOutsideDays: showOutsideDays, "aria-label": "Calendar" })) }))] }));
|
|
246
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ChangeEvent, type ComponentPropsWithRef, type ReactNode } from "react";
|
|
2
|
+
import type { CalendarType, DateRange, DateRangeInput, DateRangeRules, DateScript } from "../../Interfaces/DateInterfaces.ts";
|
|
3
|
+
import type { InputDesign, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
|
|
4
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
5
|
+
import { type CalendarProps } from "./Calendar.tsx";
|
|
6
|
+
type NativeInputProps = Omit<ComponentPropsWithRef<"input">, "type" | "size" | "value" | "defaultValue" | "min" | "max" | "step" | "prefix" | "pattern" | "minLength" | "maxLength" | "children" | keyof DateRangeRules>;
|
|
7
|
+
export interface DateRangePickerProps extends NativeInputProps, DateRangeRules, Pick<CalendarProps, "weekStart" | "weekendDays" | "isDateDisabled" | "showOutsideDays"> {
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
/** Help text under the box. */
|
|
10
|
+
hint?: ReactNode;
|
|
11
|
+
/** An error from outside, such as a server message. Shown before the field's own message. */
|
|
12
|
+
error?: string | null;
|
|
13
|
+
/** Default `standard`. */
|
|
14
|
+
design?: InputDesign;
|
|
15
|
+
/** Default `md`. */
|
|
16
|
+
size?: Size;
|
|
17
|
+
/** Decorative icon at the start. The calendar button is always at the end. */
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
/** Text before the value inside the box. */
|
|
20
|
+
prefix?: ReactNode;
|
|
21
|
+
/** The calendar the box shows and picks in: `ad` or `bs`. Default: the user's setting. The values are ISO dates either way. */
|
|
22
|
+
calendar?: CalendarType;
|
|
23
|
+
/** How each date is written in the box: `YYYY-MM-DD`, `DD/MM/YYYY`, `MMM D, YYYY`, ... Default: the user's setting. */
|
|
24
|
+
format?: string;
|
|
25
|
+
/** Digits and names of Nepali dates. Default: the user's setting. */
|
|
26
|
+
script?: DateScript;
|
|
27
|
+
/** Months shown side by side in the calendar, 1 or 2. Default 2, or 1 when the window is too narrow for two. */
|
|
28
|
+
months?: 1 | 2;
|
|
29
|
+
/** Shows a clear button while there is a value. Default true. */
|
|
30
|
+
clearable?: boolean;
|
|
31
|
+
/** Closes the calendar once the end is chosen. Default true. */
|
|
32
|
+
closeOnSelect?: boolean;
|
|
33
|
+
/** What a read-only field says when someone tries to change it. Default "Read-only: these dates cannot be changed." */
|
|
34
|
+
readOnlyMessage?: string;
|
|
35
|
+
/** Name used in messages, e.g. "Stay is required." Default: the label, when it is text. */
|
|
36
|
+
fieldName?: string;
|
|
37
|
+
/** When the field shows its own message. Default `blur`; submitting a form always shows it. */
|
|
38
|
+
validateOn?: ValidateOn;
|
|
39
|
+
/** The range as a pair of ISO dates, either `null` while not chosen, or `null` for empty. `Date`s and ISO date-times are read as days. */
|
|
40
|
+
value?: DateRangeInput;
|
|
41
|
+
defaultValue?: DateRangeInput;
|
|
42
|
+
/** After each change: the start and the end as ISO dates, `null` for an end not chosen yet; both `null` when the box is empty or holds text that is not a range. */
|
|
43
|
+
onValueChange?: (value: DateRange, event: ChangeEvent<HTMLInputElement>) => void;
|
|
44
|
+
/** Form field name of the start, e.g. `checkIn`, sent as its own field. Without it, `name` sends both dates under one name: the start, then the end. */
|
|
45
|
+
startName?: string;
|
|
46
|
+
/** Form field name of the end, e.g. `checkOut`. */
|
|
47
|
+
endName?: string;
|
|
48
|
+
/** Class of the outer field. `inputClassName` goes on the input. */
|
|
49
|
+
className?: string;
|
|
50
|
+
inputClassName?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A start and an end in one box: type them as "2026-09-17 – 2026-09-24" in the user's format, or pick them in the
|
|
54
|
+
* calendar that opens under the box, the start first and then the end, with a live preview of the days between.
|
|
55
|
+
* Shows the English (AD) or Nepali (BS) calendar from the user's settings, two months side by side when there is
|
|
56
|
+
* room, and always holds Gregorian ISO dates, which a form sends through hidden inputs (`startName` and `endName`,
|
|
57
|
+
* or both under `name`). Uncontrolled by default, so typing re-renders nothing.
|
|
58
|
+
*
|
|
59
|
+
* @example <DateRangePicker startName="checkIn" endName="checkOut" label="Stay" required min={todayIsoDate()} minDistance={1} />
|
|
60
|
+
*/
|
|
61
|
+
export declare function DateRangePicker(props: DateRangePickerProps): import("react").JSX.Element;
|
|
62
|
+
export {};
|