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,87 @@
|
|
|
1
|
+
import { type ChangeEvent, type ComponentPropsWithRef, type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import type { NumberFormatOptions, SliderMark, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
|
|
3
|
+
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
4
|
+
interface SliderBaseProps extends NumberFormatOptions {
|
|
5
|
+
label?: ReactNode;
|
|
6
|
+
/** Help text under the slider. An error takes its place. */
|
|
7
|
+
hint?: ReactNode;
|
|
8
|
+
/** An error from outside, such as a server message. */
|
|
9
|
+
error?: string | null;
|
|
10
|
+
/** Default `md`. */
|
|
11
|
+
size?: Size;
|
|
12
|
+
/** Default 0. */
|
|
13
|
+
min?: number;
|
|
14
|
+
/** Default 100. */
|
|
15
|
+
max?: number;
|
|
16
|
+
/** Default 1. */
|
|
17
|
+
step?: number;
|
|
18
|
+
/** Text before the shown value, e.g. "Rs ". Include a space if one is wanted. */
|
|
19
|
+
prefix?: string;
|
|
20
|
+
/** Text after the shown value, e.g. "%" or " nights". */
|
|
21
|
+
suffix?: string;
|
|
22
|
+
/** Writes the shown value; replaces the number format, `prefix`, and `suffix`. For example, hours as "14:30". */
|
|
23
|
+
format?: (value: number) => string;
|
|
24
|
+
/** Shows the value beside the label. Default true. */
|
|
25
|
+
showValue?: boolean;
|
|
26
|
+
/** Shows the value above the thumb while it is hovered, focused, or dragged. Best with `showValue={false}`. */
|
|
27
|
+
tooltip?: boolean;
|
|
28
|
+
/** Labelled points under the track. */
|
|
29
|
+
marks?: readonly SliderMark[];
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Can be focused but not moved; the value is still sent with a form. A lock follows the label, and a try to move it shows a note. */
|
|
32
|
+
readOnly?: boolean;
|
|
33
|
+
/** What a read-only slider says when someone tries to move it. Default "Read-only: this value cannot be changed." ("this range" for a range slider). */
|
|
34
|
+
readOnlyMessage?: string;
|
|
35
|
+
/** Form field name. A range slider sends two values under it: start, then end. */
|
|
36
|
+
name?: string;
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Name used in messages. Default: the label, when it is text. */
|
|
39
|
+
fieldName?: string;
|
|
40
|
+
/** When a `validate` message shows. Default `blur`; submitting a form always shows it. */
|
|
41
|
+
validateOn?: ValidateOn;
|
|
42
|
+
className?: string;
|
|
43
|
+
style?: CSSProperties;
|
|
44
|
+
}
|
|
45
|
+
type NativeInputProps = Omit<ComponentPropsWithRef<"input">, keyof SliderBaseProps | "type" | "value" | "defaultValue" | "onChange" | "children">;
|
|
46
|
+
export interface SliderProps extends SliderBaseProps, NativeInputProps {
|
|
47
|
+
value?: number;
|
|
48
|
+
/** Default: `min`. */
|
|
49
|
+
defaultValue?: number;
|
|
50
|
+
/** Returns a message to fail, e.g. a deposit below a minimum. */
|
|
51
|
+
validate?: (value: number) => string | null | undefined;
|
|
52
|
+
/** Every change while dragging or stepping. */
|
|
53
|
+
onValueChange?: (value: number, event: ChangeEvent<HTMLElement>) => void;
|
|
54
|
+
/** Once the thumb is released or a key step ends: the moment to call an API. */
|
|
55
|
+
onValueCommit?: (value: number) => void;
|
|
56
|
+
inputClassName?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface RangeSliderProps extends SliderBaseProps {
|
|
59
|
+
/** `[start, end]`. With `value`, the slider is controlled. */
|
|
60
|
+
value?: readonly [number, number];
|
|
61
|
+
/** Default: `[min, max]`. */
|
|
62
|
+
defaultValue?: readonly [number, number];
|
|
63
|
+
/** Smallest gap between start and end. Default 0. */
|
|
64
|
+
minDistance?: number;
|
|
65
|
+
/** Names of the two thumbs for screen readers, after the label. Default `["minimum", "maximum"]`. */
|
|
66
|
+
thumbLabels?: readonly [string, string];
|
|
67
|
+
validate?: (value: [number, number]) => string | null | undefined;
|
|
68
|
+
onValueChange?: (value: [number, number], event: ChangeEvent<HTMLElement>) => void;
|
|
69
|
+
onValueCommit?: (value: [number, number]) => void;
|
|
70
|
+
"aria-label"?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Pick a number by dragging, with the arrow keys (Page Up and Page Down for bigger steps, Home and End for the
|
|
74
|
+
* limits), or by clicking a mark. A native range input, so touch and screen readers work as usual. Dragging writes
|
|
75
|
+
* the fill and the shown value straight to the DOM: nothing re-renders.
|
|
76
|
+
*
|
|
77
|
+
* @example <Slider name="discount" label="Discount" max={50} suffix="%" defaultValue={10} />
|
|
78
|
+
*/
|
|
79
|
+
export declare function Slider({ value, defaultValue, validate, onValueChange, onValueCommit, inputClassName, ...props }: SliderProps): import("react").JSX.Element;
|
|
80
|
+
/**
|
|
81
|
+
* Pick a range with two thumbs, such as a price filter. Clicking the track moves the nearer thumb, and the thumbs
|
|
82
|
+
* keep `minDistance` apart.
|
|
83
|
+
*
|
|
84
|
+
* @example <RangeSlider name="price" label="Price per night" min={1000} max={30000} step={500} prefix="Rs " grouping />
|
|
85
|
+
*/
|
|
86
|
+
export declare function RangeSlider({ value, defaultValue, minDistance, thumbLabels, validate, onValueChange, onValueCommit, ...props }: RangeSliderProps): import("react").JSX.Element;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, useEffect, useEffectEvent, useRef, } from "react";
|
|
4
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
|
+
import { clampNumber, countDecimals, formatNumber, roundDecimal } from "../../Helpers/NumberHelpers.js";
|
|
6
|
+
import { GroupFooter, leavesField, OPTION_ATTRIBUTE, useChoiceField } from "./ChoiceField.js";
|
|
7
|
+
import { hasContent } from "./Field.js";
|
|
8
|
+
import { setInputValue } from "./InputField.js";
|
|
9
|
+
import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
|
|
10
|
+
/**
|
|
11
|
+
* Pick a number by dragging, with the arrow keys (Page Up and Page Down for bigger steps, Home and End for the
|
|
12
|
+
* limits), or by clicking a mark. A native range input, so touch and screen readers work as usual. Dragging writes
|
|
13
|
+
* the fill and the shown value straight to the DOM: nothing re-renders.
|
|
14
|
+
*
|
|
15
|
+
* @example <Slider name="discount" label="Discount" max={50} suffix="%" defaultValue={10} />
|
|
16
|
+
*/
|
|
17
|
+
export function Slider({ value, defaultValue, validate, onValueChange, onValueCommit, inputClassName, ...props }) {
|
|
18
|
+
const base = pickBase(props);
|
|
19
|
+
const inputProps = omitBase(props);
|
|
20
|
+
return (_jsx(SliderField, { ...base, values: value === undefined ? undefined : [value], defaultValues: [defaultValue ?? base.min ?? 0], validate: validate && (values => validate(values[0])), onValues: onValueChange && ((values, event) => onValueChange(values[0], event)), onCommit: onValueCommit && (values => onValueCommit(values[0])), inputProps: inputProps, inputClassName: inputClassName }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pick a range with two thumbs, such as a price filter. Clicking the track moves the nearer thumb, and the thumbs
|
|
24
|
+
* keep `minDistance` apart.
|
|
25
|
+
*
|
|
26
|
+
* @example <RangeSlider name="price" label="Price per night" min={1000} max={30000} step={500} prefix="Rs " grouping />
|
|
27
|
+
*/
|
|
28
|
+
export function RangeSlider({ value, defaultValue, minDistance = 0, thumbLabels = ["minimum", "maximum"], validate, onValueChange, onValueCommit, ...props }) {
|
|
29
|
+
const base = pickBase(props);
|
|
30
|
+
const min = base.min ?? 0;
|
|
31
|
+
const max = base.max ?? 100;
|
|
32
|
+
const start = defaultValue ?? [min, max];
|
|
33
|
+
return (_jsx(SliderField, { ...base, range: true, values: value, defaultValues: [Math.min(start[0], start[1]), Math.max(start[0], start[1])], minDistance: minDistance, thumbLabels: thumbLabels, validate: validate && (values => validate([values[0], values[1]])), onValues: onValueChange && ((values, event) => onValueChange([values[0], values[1]], event)), onCommit: onValueCommit && (values => onValueCommit([values[0], values[1]])), inputProps: { "aria-label": props["aria-label"] } }));
|
|
34
|
+
}
|
|
35
|
+
const BASE_KEYS = [
|
|
36
|
+
"label", "hint", "error", "size", "min", "max", "step", "prefix", "suffix", "format", "showValue", "tooltip", "marks", "disabled", "readOnly", "readOnlyMessage",
|
|
37
|
+
"name", "id", "fieldName", "validateOn", "className", "style", "decimals", "fixedDecimals", "grouping", "locale",
|
|
38
|
+
];
|
|
39
|
+
function pickBase(props) {
|
|
40
|
+
const base = {};
|
|
41
|
+
for (const key of BASE_KEYS)
|
|
42
|
+
if (key in props)
|
|
43
|
+
base[key] = props[key];
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
function omitBase(props) {
|
|
47
|
+
const rest = { ...props };
|
|
48
|
+
for (const key of BASE_KEYS)
|
|
49
|
+
delete rest[key];
|
|
50
|
+
return rest;
|
|
51
|
+
}
|
|
52
|
+
function SliderField(props) {
|
|
53
|
+
const { label, hint, size = "md", min = 0, max = 100, step = 1, prefix, suffix, format, showValue = true, tooltip = false, marks, disabled = false, readOnly = false, name, fieldName, className, style, decimals, fixedDecimals, grouping, locale, range = false, values, defaultValues, minDistance = 0, thumbLabels = [], validate, onValues, onCommit, inputProps, inputClassName, } = props;
|
|
54
|
+
const { readOnlyMessage = range ? "Read-only: this range cannot be changed." : "Read-only: this value cannot be changed." } = props;
|
|
55
|
+
const span = max - min || 1;
|
|
56
|
+
const initial = (values ?? defaultValues).map(value => clampNumber(value, min, max));
|
|
57
|
+
const text = (value) => (format ? format(value) : `${prefix ?? ""}${formatNumber(value, { decimals, fixedDecimals, grouping, locale })}${suffix ?? ""}`);
|
|
58
|
+
const fraction = (value) => (clampNumber(value, min, max) - min) / span;
|
|
59
|
+
const controlRef = useRef(null);
|
|
60
|
+
const valueRef = useRef(null);
|
|
61
|
+
const tooltipRefs = useRef([]);
|
|
62
|
+
// The last values, so a read-only slider can put a moved thumb straight back.
|
|
63
|
+
const lastRef = useRef(initial);
|
|
64
|
+
const field = useChoiceField(props, {
|
|
65
|
+
check: inputs => validate?.(read(inputs)) || null,
|
|
66
|
+
checkKeys: [values?.join(), validate, min, max],
|
|
67
|
+
sync,
|
|
68
|
+
});
|
|
69
|
+
const read = (inputs) => inputs.map(input => Number(input.value));
|
|
70
|
+
// A read-only slider puts a moved thumb straight back: a lock after the label, and a note under the track, say why.
|
|
71
|
+
const readOnlyNotice = useReadOnlyNotice(readOnly && !disabled, field.containerRef, readOnlyMessage, {
|
|
72
|
+
anchor: target => target.closest(".nx-slider__control"),
|
|
73
|
+
typing: false,
|
|
74
|
+
keys: ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End"],
|
|
75
|
+
lock: "label",
|
|
76
|
+
});
|
|
77
|
+
/** Writes the fill, the shown value, the tooltips, and each thumb's spoken value straight to the DOM. */
|
|
78
|
+
function sync(inputs) {
|
|
79
|
+
const current = read(inputs);
|
|
80
|
+
const control = controlRef.current;
|
|
81
|
+
if (!control || current.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
control.style.setProperty("--nx-slider-start", String(range ? fraction(current[0]) : 0));
|
|
84
|
+
control.style.setProperty("--nx-slider-end", String(fraction(current[current.length - 1])));
|
|
85
|
+
inputs.forEach((input, index) => {
|
|
86
|
+
const valueText = text(current[index]);
|
|
87
|
+
input.setAttribute("aria-valuetext", valueText);
|
|
88
|
+
const bubble = tooltipRefs.current[index];
|
|
89
|
+
if (bubble) {
|
|
90
|
+
bubble.textContent = valueText;
|
|
91
|
+
bubble.style.setProperty("--nx-slider-at", String(fraction(current[index])));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (valueRef.current)
|
|
95
|
+
valueRef.current.textContent = current.map(text).join(" – ");
|
|
96
|
+
lastRef.current = current;
|
|
97
|
+
}
|
|
98
|
+
function handleChange(event) {
|
|
99
|
+
const input = event.target;
|
|
100
|
+
if (!(input instanceof HTMLInputElement) || !input.hasAttribute(OPTION_ATTRIBUTE))
|
|
101
|
+
return;
|
|
102
|
+
const index = Number(input.getAttribute(OPTION_ATTRIBUTE));
|
|
103
|
+
if (readOnly) {
|
|
104
|
+
input.value = String(lastRef.current[index]);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const inputs = field.inputs();
|
|
108
|
+
if (range && inputs.length === 2) {
|
|
109
|
+
// The thumbs never cross, and keep minDistance apart.
|
|
110
|
+
const [start, end] = inputs;
|
|
111
|
+
if (index === 0 && Number(start.value) > Number(end.value) - minDistance)
|
|
112
|
+
start.value = String(Math.max(min, Number(end.value) - minDistance));
|
|
113
|
+
if (index === 1 && Number(end.value) < Number(start.value) + minDistance)
|
|
114
|
+
end.value = String(Math.min(max, Number(start.value) + minDistance));
|
|
115
|
+
controlRef.current?.setAttribute("data-active", String(index));
|
|
116
|
+
}
|
|
117
|
+
field.onChange();
|
|
118
|
+
onValues?.(read(inputs), event);
|
|
119
|
+
}
|
|
120
|
+
// A released thumb, or a finished key step, fires the native change event.
|
|
121
|
+
const commit = useEffectEvent(() => onCommit?.(read(field.inputs())));
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const control = controlRef.current;
|
|
124
|
+
if (!control)
|
|
125
|
+
return;
|
|
126
|
+
const onNativeChange = () => commit();
|
|
127
|
+
control.addEventListener("change", onNativeChange);
|
|
128
|
+
return () => control.removeEventListener("change", onNativeChange);
|
|
129
|
+
}, []);
|
|
130
|
+
/** The value under a pointer's x position, on the step grid. */
|
|
131
|
+
function valueAt(clientX) {
|
|
132
|
+
const control = controlRef.current;
|
|
133
|
+
const rect = control.getBoundingClientRect();
|
|
134
|
+
const thumb = parseFloat(getComputedStyle(control).getPropertyValue("--nx-slider-thumb")) || 16;
|
|
135
|
+
const position = clampNumber((clientX - rect.left - thumb / 2) / Math.max(1, rect.width - thumb), 0, 1);
|
|
136
|
+
const stepped = min + Math.round((position * span) / step) * step;
|
|
137
|
+
return clampNumber(roundDecimal(stepped, Math.max(countDecimals(step), countDecimals(min))), min, max);
|
|
138
|
+
}
|
|
139
|
+
/** Moves the thumb nearest to a value there, as a click on the track or on a mark does. */
|
|
140
|
+
function moveTo(value, focus = true) {
|
|
141
|
+
const input = nearestInput(value);
|
|
142
|
+
if (disabled || readOnly || !input)
|
|
143
|
+
return null;
|
|
144
|
+
if (focus)
|
|
145
|
+
input.focus();
|
|
146
|
+
setInputValue(input, String(value));
|
|
147
|
+
return input;
|
|
148
|
+
}
|
|
149
|
+
/** The thumb a press at a value would move: the only one, or the nearer of two. */
|
|
150
|
+
function nearestInput(value) {
|
|
151
|
+
const inputs = field.inputs();
|
|
152
|
+
if (inputs.length < 2)
|
|
153
|
+
return inputs[0];
|
|
154
|
+
const [start, end] = read(inputs);
|
|
155
|
+
return inputs[value <= start ? 0 : value >= end ? 1 : value - start <= end - value ? 0 : 1];
|
|
156
|
+
}
|
|
157
|
+
function commitInput(input) {
|
|
158
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
159
|
+
}
|
|
160
|
+
function handleControlPointerDown(event) {
|
|
161
|
+
if (!readOnly) {
|
|
162
|
+
if (range)
|
|
163
|
+
handleTrackPointerDown(event);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// A press on a range slider's track focuses the nearer thumb, as when editable, so focus stays in the slider
|
|
167
|
+
// and the note stays up.
|
|
168
|
+
if (range && event.button === 0 && !(event.target instanceof HTMLInputElement)) {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
nearestInput(valueAt(event.clientX))?.focus();
|
|
171
|
+
}
|
|
172
|
+
readOnlyNotice.show(event.currentTarget);
|
|
173
|
+
}
|
|
174
|
+
function handleBlur(event) {
|
|
175
|
+
if (leavesField(event))
|
|
176
|
+
readOnlyNotice.hide();
|
|
177
|
+
field.onBlur(event);
|
|
178
|
+
}
|
|
179
|
+
// A range slider's inputs only take pointer events on their thumbs, so a press on the track lands here.
|
|
180
|
+
function handleTrackPointerDown(event) {
|
|
181
|
+
if (event.button !== 0 || event.target instanceof HTMLInputElement)
|
|
182
|
+
return;
|
|
183
|
+
const input = moveTo(valueAt(event.clientX));
|
|
184
|
+
if (!input)
|
|
185
|
+
return;
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
const control = event.currentTarget;
|
|
188
|
+
control.setPointerCapture(event.pointerId);
|
|
189
|
+
const move = (moveEvent) => setInputValue(input, String(valueAt(moveEvent.clientX)));
|
|
190
|
+
const end = () => {
|
|
191
|
+
control.removeEventListener("pointermove", move);
|
|
192
|
+
control.removeEventListener("lostpointercapture", end);
|
|
193
|
+
commitInput(input);
|
|
194
|
+
};
|
|
195
|
+
control.addEventListener("pointermove", move);
|
|
196
|
+
control.addEventListener("lostpointercapture", end);
|
|
197
|
+
}
|
|
198
|
+
const labelId = `${field.fieldId}-label`;
|
|
199
|
+
const inputId = (index) => (index === 0 ? field.fieldId : `${field.fieldId}-${index}`);
|
|
200
|
+
const describedBy = field.error || hint ? field.messageId : undefined;
|
|
201
|
+
const nameText = fieldName ?? (typeof label === "string" ? label : undefined);
|
|
202
|
+
const controlStyle = { "--nx-slider-start": range ? fraction(initial[0]) : 0, "--nx-slider-end": fraction(initial[initial.length - 1]) };
|
|
203
|
+
return (_jsxs("div", { ref: field.containerRef, role: range ? "group" : undefined, "aria-labelledby": range && hasContent(label) ? labelId : undefined, className: cx("nx-slider", `nx-slider--${size}`, range && "nx-slider--range", className), style: style, "data-invalid": field.error ? true : undefined, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, onChange: handleChange, onKeyDown: readOnlyNotice.onKeyDown, onBlur: handleBlur, children: [(hasContent(label) || showValue) && (_jsxs("div", { className: "nx-slider__head", children: [hasContent(label) &&
|
|
204
|
+
(range ? (_jsxs("span", { id: labelId, className: "nx-field__label", children: [label, readOnlyNotice.lock] })) : (_jsxs("label", { id: labelId, htmlFor: field.fieldId, className: "nx-field__label", children: [label, readOnlyNotice.lock] }))), showValue && (_jsx("output", { ref: valueRef, className: "nx-slider__value", htmlFor: initial.map((_, index) => inputId(index)).join(" "), "aria-hidden": "true", children: initial.map(text).join(" – ") }))] })), _jsxs("div", { ref: controlRef, className: "nx-slider__control", style: controlStyle, onPointerDown: range || readOnly ? handleControlPointerDown : undefined, children: [_jsx("span", { className: "nx-slider__track", "aria-hidden": "true", children: _jsx("span", { className: "nx-slider__fill" }) }), initial.map((value, index) => (_jsxs(Fragment, { children: [_jsx("input", { ...(index === 0 ? inputProps : undefined), [OPTION_ATTRIBUTE]: index, id: inputId(index), type: "range", className: cx("nx-slider__input", inputClassName), name: name, min: min, max: max, step: step, value: values ? value : undefined, defaultValue: values ? undefined : value, disabled: disabled, "aria-label": range ? [nameText, thumbLabels[index]].filter(Boolean).join(" ") || undefined : inputProps["aria-label"], "aria-readonly": readOnly || undefined, "aria-invalid": field.error ? true : undefined, "aria-describedby": describedBy, "aria-valuetext": text(value), onInvalid: field.onInvalid,
|
|
205
|
+
// A controlled value changes through onValueChange; this silences React's missing-onChange warning.
|
|
206
|
+
onChange: values ? noop : undefined }), tooltip && (_jsx("span", { ref: node => {
|
|
207
|
+
tooltipRefs.current[index] = node;
|
|
208
|
+
}, className: "nx-slider__tooltip", style: { "--nx-slider-at": fraction(value) }, "aria-hidden": "true", children: text(value) }))] }, index)))] }), marks && marks.length > 0 && (_jsx("div", { className: "nx-slider__marks", "aria-hidden": "true", children: marks.map(mark => (_jsx("button", { type: "button", tabIndex: -1, className: "nx-slider__mark", style: { "--nx-slider-at": fraction(mark.value) }, "data-edge": mark.value <= min ? "start" : mark.value >= max ? "end" : undefined, disabled: disabled, onClick: () => {
|
|
209
|
+
const input = moveTo(mark.value);
|
|
210
|
+
if (input)
|
|
211
|
+
commitInput(input);
|
|
212
|
+
else
|
|
213
|
+
readOnlyNotice.show(controlRef.current);
|
|
214
|
+
}, children: mark.label ?? text(mark.value) }, mark.value))) })), _jsx(GroupFooter, { id: field.messageId, hint: hint, error: field.error }), readOnlyNotice.notice] }));
|
|
215
|
+
}
|
|
216
|
+
function noop() { }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RowValue, TabularFormProps } from "../../Interfaces/FormInterfaces.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
|
|
4
|
+
* value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
|
|
5
|
+
* come as params, the same objects a form takes, so the same list can show as sections in a `GroupForm`.
|
|
6
|
+
*
|
|
7
|
+
* Only the rows in view are on the page, so a sheet of thousands of rows opens and types as fast as a short one; the
|
|
8
|
+
* values live in a store outside React, and typing renders nothing. The header, the row numbers, and the remove buttons
|
|
9
|
+
* stay in place while the table scrolls. A row number opens the row's menu (insert, duplicate, move, remove). Enter in
|
|
10
|
+
* a text or number cell moves down the column, and on the last row adds a row.
|
|
11
|
+
*
|
|
12
|
+
* Inside a `<form>`, the rows are sent as JSON under `name`; a row with a problem blocks submit, and the first one
|
|
13
|
+
* scrolls into view with focus.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
|
|
17
|
+
* { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
|
|
18
|
+
* { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
|
|
19
|
+
* { type: "checkbox", name: "isVerified", label: "Verified" },
|
|
20
|
+
* { type: "radio", name: "isPrimary", label: "Primary", required: true },
|
|
21
|
+
* ]} />
|
|
22
|
+
*/
|
|
23
|
+
export declare function TabularForm<TRow extends RowValue = RowValue>(props: TabularFormProps<TRow>): import("react").JSX.Element;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconRowInsertBottom, iconRowInsertTop, iconTrash, iconX, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { Button } from "../Documents/Button.js";
|
|
7
|
+
import { Menu } from "../Documents/Menu.js";
|
|
8
|
+
import { firstFocusable, labelText, RowCell, rowName, RowsFooter, RowsLegend, useRowsFrame, useRowsInput, useRowVersion } from "./RowsInput.js";
|
|
9
|
+
/** Height of a row: the input's box and the line under it. Measured from the first row once it renders. */
|
|
10
|
+
const ROW_HEIGHT = { sm: 29, md: 35, lg: 41 };
|
|
11
|
+
/** Rows rendered above and below the ones in view, so Tab and Enter always find the next row on the page. */
|
|
12
|
+
const OVERSCAN = 8;
|
|
13
|
+
const DEFAULT_MAX_HEIGHT = 440;
|
|
14
|
+
/** A column's width by input type, when its params give none. */
|
|
15
|
+
const COLUMN_WIDTH = {
|
|
16
|
+
text: "10rem",
|
|
17
|
+
email: "13rem",
|
|
18
|
+
password: "10rem",
|
|
19
|
+
search: "10rem",
|
|
20
|
+
tel: "9rem",
|
|
21
|
+
url: "13rem",
|
|
22
|
+
textarea: "14rem",
|
|
23
|
+
number: "7.5rem",
|
|
24
|
+
checkbox: "6rem",
|
|
25
|
+
switch: "6rem",
|
|
26
|
+
radio: "6rem",
|
|
27
|
+
"checkbox-group": "16rem",
|
|
28
|
+
"radio-group": "16rem",
|
|
29
|
+
dropdown: "10rem",
|
|
30
|
+
date: "10rem",
|
|
31
|
+
"date-range": "15rem",
|
|
32
|
+
time: "8rem",
|
|
33
|
+
"date-time": "14rem",
|
|
34
|
+
slider: "10rem",
|
|
35
|
+
"range-slider": "12rem",
|
|
36
|
+
};
|
|
37
|
+
/** Columns that keep their width instead of sharing the spare room. */
|
|
38
|
+
const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
|
|
39
|
+
const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
|
|
40
|
+
const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
|
|
41
|
+
/** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
|
|
42
|
+
function cellKind(params) {
|
|
43
|
+
if (params.type === "number")
|
|
44
|
+
return "number";
|
|
45
|
+
if (FIXED_TYPES.has(params.type))
|
|
46
|
+
return "check";
|
|
47
|
+
return "text";
|
|
48
|
+
}
|
|
49
|
+
function columnWidth(params) {
|
|
50
|
+
return cssLength(params.width ?? COLUMN_WIDTH[params.type] ?? "12rem");
|
|
51
|
+
}
|
|
52
|
+
/** One row of inputs. It renders again only when its position, its messages, or a value set from code change. */
|
|
53
|
+
const TabularRow = memo(function TabularRow({ store, rowKey, index, count, columns, context, onAction }) {
|
|
54
|
+
useRowVersion(store, rowKey);
|
|
55
|
+
const name = rowName(store, index);
|
|
56
|
+
const title = `${store.names.title} ${index + 1}`;
|
|
57
|
+
const invalid = context.showAll && store.rowProblems(rowKey) > 0;
|
|
58
|
+
const items = [];
|
|
59
|
+
if (context.canAdd) {
|
|
60
|
+
items.push({ value: "insert-above", label: "Insert above", icon: _jsx(InlineIcon, { icon: iconRowInsertTop }) }, { value: "insert-below", label: "Insert below", icon: _jsx(InlineIcon, { icon: iconRowInsertBottom }) }, { value: "duplicate", label: "Duplicate", icon: _jsx(InlineIcon, { icon: iconCopy }) });
|
|
61
|
+
}
|
|
62
|
+
if (context.sortable) {
|
|
63
|
+
if (items.length)
|
|
64
|
+
items.push({ separator: true });
|
|
65
|
+
items.push({ value: "move-up", label: "Move up", icon: _jsx(InlineIcon, { icon: iconArrowUp }), disabled: index === 0 }, { value: "move-down", label: "Move down", icon: _jsx(InlineIcon, { icon: iconArrowDown }), disabled: index === count - 1 });
|
|
66
|
+
}
|
|
67
|
+
if (context.removable) {
|
|
68
|
+
if (items.length)
|
|
69
|
+
items.push({ separator: true });
|
|
70
|
+
items.push({ value: "remove", label: "Remove", icon: _jsx(InlineIcon, { icon: iconTrash }), tone: "danger" });
|
|
71
|
+
}
|
|
72
|
+
// Leaving an input counts as using it: a repeated value shows its message from then on.
|
|
73
|
+
function onBlur(event) {
|
|
74
|
+
const cell = event.target.closest("[data-nx-cell]");
|
|
75
|
+
if (!cell || (event.relatedTarget instanceof Node && cell.contains(event.relatedTarget)))
|
|
76
|
+
return;
|
|
77
|
+
store.touch(rowKey, cell.dataset.nxCell ?? "");
|
|
78
|
+
}
|
|
79
|
+
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsx("div", { role: "cell", className: "nx-tabular__num", children: items.length > (context.removable ? 1 : 0) ? (_jsx(Menu, { items: items, onSelect: value => onAction(value, rowKey), "aria-label": `${title} actions`, children: _jsxs("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${title} actions`, title: `${title} actions`, "data-nx-row-actions": "", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), _jsx(InlineIcon, { icon: iconDotsVertical, className: "nx-tabular__num-icon" })] }) })) : (_jsx("span", { className: "nx-tabular__num-text", children: index + 1 })) })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: params, index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled }, store.revision(rowKey, params.name)) }, params.name))), context.removable && (_jsx("div", { role: "cell", className: "nx-tabular__end", children: _jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", className: "nx-tabular__remove", icon: _jsx(InlineIcon, { icon: iconX }), "aria-label": `Remove ${name}`, title: `Remove ${name}`, onClick: () => onAction("remove", rowKey) }) }))] }));
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
|
|
83
|
+
* value is an array of row objects, one value per input, with any other fields the rows came with (such as ids). Inputs
|
|
84
|
+
* come as params, the same objects a form takes, so the same list can show as sections in a `GroupForm`.
|
|
85
|
+
*
|
|
86
|
+
* Only the rows in view are on the page, so a sheet of thousands of rows opens and types as fast as a short one; the
|
|
87
|
+
* values live in a store outside React, and typing renders nothing. The header, the row numbers, and the remove buttons
|
|
88
|
+
* stay in place while the table scrolls. A row number opens the row's menu (insert, duplicate, move, remove). Enter in
|
|
89
|
+
* a text or number cell moves down the column, and on the last row adds a row.
|
|
90
|
+
*
|
|
91
|
+
* Inside a `<form>`, the rows are sent as JSON under `name`; a row with a problem blocks submit, and the first one
|
|
92
|
+
* scrolls into view with focus.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* <TabularForm name="emails" label="Emails" itemLabel="Email" max={5} unique={["email"]} inputs={[
|
|
96
|
+
* { type: "email", name: "email", label: "Email", required: true, pattern: TEXT_PATTERNS.email },
|
|
97
|
+
* { type: "dropdown", name: "type", label: "Type", options: EMAIL_TYPES },
|
|
98
|
+
* { type: "checkbox", name: "isVerified", label: "Verified" },
|
|
99
|
+
* { type: "radio", name: "isPrimary", label: "Primary", required: true },
|
|
100
|
+
* ]} />
|
|
101
|
+
*/
|
|
102
|
+
export function TabularForm(props) {
|
|
103
|
+
const { inputs, label, hint, error, size = "md", maxHeight = DEFAULT_MAX_HEIGHT, numbered = true, readOnly = false, disabled = false, required, max, className, style } = props;
|
|
104
|
+
const addable = props.addable !== false && !readOnly;
|
|
105
|
+
const removable = props.removable !== false && !readOnly && !disabled;
|
|
106
|
+
const sortable = Boolean(props.sortable) && !readOnly && !disabled;
|
|
107
|
+
const layoutRef = useRef(null);
|
|
108
|
+
const rows = useRowsInput(props, "Row", layoutRef);
|
|
109
|
+
const store = rows.store;
|
|
110
|
+
const { snapshot } = rows;
|
|
111
|
+
const frame = useRowsFrame(props, store);
|
|
112
|
+
const columns = useMemo(() => inputs.filter(params => !params.hidden), [inputs]);
|
|
113
|
+
const count = snapshot.keys.length;
|
|
114
|
+
const viewportRef = useRef(null);
|
|
115
|
+
const headRef = useRef(null);
|
|
116
|
+
const bodyRef = useRef(null);
|
|
117
|
+
const [rowHeight, setRowHeight] = useState(ROW_HEIGHT[size]);
|
|
118
|
+
const pageRows = Math.ceil((typeof maxHeight === "number" ? maxHeight : 600) / rowHeight);
|
|
119
|
+
const [range, setRange] = useState({ start: 0, end: pageRows + OVERSCAN });
|
|
120
|
+
const [status, setStatus] = useState(null);
|
|
121
|
+
const pendingRef = useRef(null);
|
|
122
|
+
const start = Math.min(range.start, count);
|
|
123
|
+
const end = Math.min(count, Math.max(range.end, start + 1));
|
|
124
|
+
const shownKeys = snapshot.keys.slice(start, end);
|
|
125
|
+
/** The rows in view, from the scroll position, and a margin around them. */
|
|
126
|
+
const updateRange = useCallback(() => {
|
|
127
|
+
const viewport = viewportRef.current;
|
|
128
|
+
if (!viewport)
|
|
129
|
+
return;
|
|
130
|
+
const head = headRef.current?.offsetHeight ?? 0;
|
|
131
|
+
const top = Math.max(0, viewport.scrollTop - head);
|
|
132
|
+
const first = Math.max(0, Math.floor(top / rowHeight) - OVERSCAN);
|
|
133
|
+
const last = Math.ceil((top + Math.max(viewport.clientHeight - head, rowHeight)) / rowHeight) + OVERSCAN;
|
|
134
|
+
// Focus in a row that is about to leave the page moves to the table, so the keyboard keeps its place.
|
|
135
|
+
const active = document.activeElement;
|
|
136
|
+
const activeRow = active instanceof Element ? active.closest("[data-nx-row]") : null;
|
|
137
|
+
if (activeRow && bodyRef.current?.contains(activeRow)) {
|
|
138
|
+
const index = store.indexOf(activeRow.dataset.nxRow ?? "");
|
|
139
|
+
if (index >= 0 && (index < first || index >= last))
|
|
140
|
+
viewport.focus({ preventScroll: true });
|
|
141
|
+
}
|
|
142
|
+
setRange(current => (current.start === first && current.end === last ? current : { start: first, end: last }));
|
|
143
|
+
}, [rowHeight, store]);
|
|
144
|
+
// The real row height, once a row is on the page (custom styles may change it).
|
|
145
|
+
useLayoutEffect(() => {
|
|
146
|
+
const row = bodyRef.current?.querySelector(":scope > .nx-tabular__row");
|
|
147
|
+
const height = row?.offsetHeight;
|
|
148
|
+
if (height && height !== rowHeight)
|
|
149
|
+
setRowHeight(height);
|
|
150
|
+
});
|
|
151
|
+
// A resize, a new row height, or fewer rows change which rows are in view.
|
|
152
|
+
useLayoutEffect(() => {
|
|
153
|
+
const viewport = viewportRef.current;
|
|
154
|
+
if (!viewport)
|
|
155
|
+
return;
|
|
156
|
+
updateRange();
|
|
157
|
+
const observer = new ResizeObserver(updateRange);
|
|
158
|
+
observer.observe(viewport);
|
|
159
|
+
return () => observer.disconnect();
|
|
160
|
+
}, [updateRange, count]);
|
|
161
|
+
function tryFocus() {
|
|
162
|
+
const pending = pendingRef.current;
|
|
163
|
+
const body = bodyRef.current;
|
|
164
|
+
if (!pending || !body)
|
|
165
|
+
return;
|
|
166
|
+
const row = body.querySelector(`:scope > [data-nx-row="${CSS.escape(pending.key)}"]`);
|
|
167
|
+
if (!row)
|
|
168
|
+
return;
|
|
169
|
+
pendingRef.current = null;
|
|
170
|
+
const { target } = pending;
|
|
171
|
+
let element;
|
|
172
|
+
if (typeof target === "string")
|
|
173
|
+
element = firstFocusable(row.querySelector(`[data-nx-cell="${CSS.escape(target)}"]`));
|
|
174
|
+
else if ("actions" in target)
|
|
175
|
+
element = row.querySelector("[data-nx-row-actions]") ?? firstFocusable(row);
|
|
176
|
+
else
|
|
177
|
+
element = firstFocusable(row.querySelector("[data-nx-cell]")) ?? firstFocusable(row);
|
|
178
|
+
element?.focus();
|
|
179
|
+
}
|
|
180
|
+
layoutRef.current = {
|
|
181
|
+
focusRow(key, target) {
|
|
182
|
+
const index = store.indexOf(key);
|
|
183
|
+
const viewport = viewportRef.current;
|
|
184
|
+
if (index < 0 || !viewport)
|
|
185
|
+
return;
|
|
186
|
+
pendingRef.current = { key, target };
|
|
187
|
+
// Scroll the row into view below the header, and render the rows around it before the scroll event comes.
|
|
188
|
+
const head = headRef.current?.offsetHeight ?? 0;
|
|
189
|
+
const top = index * rowHeight;
|
|
190
|
+
if (top < viewport.scrollTop)
|
|
191
|
+
viewport.scrollTop = top;
|
|
192
|
+
else if (head + top + rowHeight > viewport.scrollTop + viewport.clientHeight)
|
|
193
|
+
viewport.scrollTop = head + top + rowHeight - viewport.clientHeight;
|
|
194
|
+
setRange(current => (index >= current.start && index < current.end ? current : { start: Math.max(0, index - OVERSCAN), end: index + pageRows + OVERSCAN }));
|
|
195
|
+
// After the render that brings the row (a layout effect), or in the next frame when nothing renders.
|
|
196
|
+
requestAnimationFrame(tryFocus);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
useLayoutEffect(tryFocus);
|
|
200
|
+
const actionsRef = useRef(rows.actions);
|
|
201
|
+
actionsRef.current = rows.actions;
|
|
202
|
+
const onAction = useCallback((action, key) => {
|
|
203
|
+
const index = store.indexOf(key);
|
|
204
|
+
const run = actionsRef.current;
|
|
205
|
+
if (action === "insert-above")
|
|
206
|
+
run.add(undefined, index);
|
|
207
|
+
else if (action === "insert-below")
|
|
208
|
+
run.add(undefined, index + 1);
|
|
209
|
+
else if (action === "duplicate")
|
|
210
|
+
run.duplicate(key);
|
|
211
|
+
else if (action === "move-up")
|
|
212
|
+
run.move(key, -1);
|
|
213
|
+
else if (action === "move-down")
|
|
214
|
+
run.move(key, 1);
|
|
215
|
+
else
|
|
216
|
+
run.remove(key);
|
|
217
|
+
}, [store]);
|
|
218
|
+
/** The message or hint of the input with focus, shown under the table: cells have no room for their own. */
|
|
219
|
+
function readStatus() {
|
|
220
|
+
const active = document.activeElement;
|
|
221
|
+
const cell = active instanceof Element ? active.closest("[data-nx-cell]") : null;
|
|
222
|
+
const row = cell?.closest("[data-nx-row]");
|
|
223
|
+
let next = null;
|
|
224
|
+
if (cell && row && bodyRef.current?.contains(cell)) {
|
|
225
|
+
const message = cell.querySelector(".nx-field__message");
|
|
226
|
+
const text = message?.textContent?.trim() ?? "";
|
|
227
|
+
const isError = Boolean(message?.classList.contains("nx-field__message--error"));
|
|
228
|
+
const params = columns.find(item => item.name === cell.dataset.nxCell);
|
|
229
|
+
if (text)
|
|
230
|
+
next = { text: isError ? `${store.names.title} ${store.indexOf(row.dataset.nxRow ?? "") + 1}: ${text}` : `${params ? labelText(params) : ""}: ${text}`, error: isError };
|
|
231
|
+
}
|
|
232
|
+
setStatus(current => (current?.text === next?.text && current?.error === next?.error ? current : next));
|
|
233
|
+
}
|
|
234
|
+
const readStatusSoon = () => requestAnimationFrame(readStatus);
|
|
235
|
+
useLayoutEffect(() => {
|
|
236
|
+
if (status)
|
|
237
|
+
readStatus();
|
|
238
|
+
});
|
|
239
|
+
/** Enter moves down the column, Shift+Enter up; on the last row Enter adds a row. */
|
|
240
|
+
function onBodyKeyDown(event) {
|
|
241
|
+
if (event.key !== "Enter" || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
242
|
+
return;
|
|
243
|
+
const target = event.target;
|
|
244
|
+
if (!(target instanceof HTMLInputElement) || !ENTER_MOVES.has(target.type) || target.hasAttribute("aria-haspopup") || target.getAttribute("role") === "combobox")
|
|
245
|
+
return;
|
|
246
|
+
const cell = target.closest("[data-nx-cell]");
|
|
247
|
+
const row = target.closest("[data-nx-row]");
|
|
248
|
+
if (!cell || !row)
|
|
249
|
+
return;
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
const name = cell.dataset.nxCell ?? "";
|
|
252
|
+
const index = store.indexOf(row.dataset.nxRow ?? "") + (event.shiftKey ? -1 : 1);
|
|
253
|
+
if (index < 0)
|
|
254
|
+
return;
|
|
255
|
+
const next = store.keyAt(index);
|
|
256
|
+
if (next)
|
|
257
|
+
layoutRef.current?.focusRow(next, name);
|
|
258
|
+
else if (!event.shiftKey && rows.canAdd) {
|
|
259
|
+
const key = store.add();
|
|
260
|
+
layoutRef.current?.focusRow(key, name);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll]);
|
|
264
|
+
const numberWidth = `calc(${Math.max(2, String(count).length)}ch + 24px)`;
|
|
265
|
+
const widths = columns.map(columnWidth);
|
|
266
|
+
const template = [
|
|
267
|
+
numbered ? numberWidth : null,
|
|
268
|
+
...columns.map((params, index) => {
|
|
269
|
+
const grow = params.grow ?? (FIXED_TYPES.has(params.type) ? 0 : 1);
|
|
270
|
+
return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
|
|
271
|
+
}),
|
|
272
|
+
removable ? "var(--nx-tabular-end)" : null,
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join(" ");
|
|
276
|
+
const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, removable ? "var(--nx-tabular-end)" : "0px"].join(" + ")})`;
|
|
277
|
+
const tableStyle = { "--nx-tabular-columns": template, minWidth };
|
|
278
|
+
const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
|
|
279
|
+
return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1, style: { maxHeight: cssLength(maxHeight) }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
|
|
280
|
+
const columnHint = params.hint;
|
|
281
|
+
return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: typeof columnHint === "string" ? columnHint : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" }))] }, params.name));
|
|
282
|
+
}), removable && (_jsx("div", { role: "columnheader", className: "nx-tabular__end", children: _jsx("span", { className: "nx-visually-hidden", children: "Remove" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
|
|
283
|
+
}
|