@inkra/react 0.1.0 → 0.3.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/README.md +186 -1
- package/dist/PencilIcon.js +14 -16
- package/dist/PencilLine.js +18 -12
- package/dist/PencilText.js +192 -59
- package/dist/alert-dialog/index.d.ts +64 -0
- package/dist/alert-dialog/index.js +111 -0
- package/dist/audio-player/index.d.ts +60 -0
- package/dist/audio-player/index.js +101 -0
- package/dist/carousel/index.d.ts +51 -0
- package/dist/carousel/index.js +290 -0
- package/dist/choice-grid/index.d.ts +38 -0
- package/dist/choice-grid/index.js +85 -0
- package/dist/color-picker/color.d.ts +15 -0
- package/dist/color-picker/color.js +55 -0
- package/dist/color-picker/index.d.ts +31 -0
- package/dist/color-picker/index.js +69 -0
- package/dist/command/index.d.ts +45 -0
- package/dist/command/index.js +39 -0
- package/dist/context-menu/index.d.ts +76 -0
- package/dist/context-menu/index.js +241 -0
- package/dist/data-table/index.d.ts +47 -0
- package/dist/data-table/index.js +91 -0
- package/dist/date-picker/index.d.ts +50 -0
- package/dist/date-picker/index.js +180 -0
- package/dist/display/ProgressRing.d.ts +6 -0
- package/dist/display/ProgressRing.js +146 -0
- package/dist/display/index.d.ts +27 -0
- package/dist/display/index.js +18 -2
- package/dist/dockable-panel/index.d.ts +41 -0
- package/dist/dockable-panel/index.js +253 -0
- package/dist/dockable-panel/model.d.ts +21 -0
- package/dist/dockable-panel/model.js +61 -0
- package/dist/drawer/index.d.ts +63 -0
- package/dist/drawer/index.js +88 -0
- package/dist/dropdown-menu/index.d.ts +1 -1
- package/dist/dropdown-menu/index.js +3 -2
- package/dist/editable/index.d.ts +53 -0
- package/dist/editable/index.js +162 -0
- package/dist/empty/index.d.ts +21 -0
- package/dist/empty/index.js +23 -0
- package/dist/flip/index.d.ts +49 -0
- package/dist/flip/index.js +138 -0
- package/dist/form/EditablePencilText.js +188 -143
- package/dist/form/Input.js +1 -1
- package/dist/form/Textarea.d.ts +12 -0
- package/dist/form/Textarea.js +83 -4
- package/dist/form/index.d.ts +1 -1
- package/dist/heatmap/index.d.ts +59 -0
- package/dist/heatmap/index.js +248 -0
- package/dist/hotkey/index.d.ts +37 -0
- package/dist/hotkey/index.js +130 -0
- package/dist/hotkey/model.d.ts +27 -0
- package/dist/hotkey/model.js +97 -0
- package/dist/hover-card/index.d.ts +33 -0
- package/dist/hover-card/index.js +141 -0
- package/dist/image-cropper/index.d.ts +82 -0
- package/dist/image-cropper/index.js +261 -0
- package/dist/image-cropper/model.d.ts +42 -0
- package/dist/image-cropper/model.js +94 -0
- package/dist/image-viewer/index.d.ts +79 -0
- package/dist/image-viewer/index.js +220 -0
- package/dist/image-viewer/model.d.ts +9 -0
- package/dist/image-viewer/model.js +18 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/input-number/index.d.ts +6 -0
- package/dist/input-number/index.js +126 -15
- package/dist/input-number/model.d.ts +13 -0
- package/dist/input-number/model.js +50 -0
- package/dist/internal/cache.d.ts +1 -1
- package/dist/internal/cache.js +1 -1
- package/dist/internal/renderTokens.d.ts +10 -0
- package/dist/internal/renderTokens.js +13 -0
- package/dist/internal/surfaceRenderer.d.ts +14 -6
- package/dist/internal/surfaceRenderer.js +15 -12
- package/dist/internal/textRenderer.d.ts +1 -0
- package/dist/internal/textRenderer.js +33 -7
- package/dist/internal/usePencilSurface.js +16 -8
- package/dist/internal/useSelectionDrawing.js +15 -11
- package/dist/journey-map/MapScale.js +3 -1
- package/dist/journey-map/index.js +3 -1
- package/dist/keypad/index.d.ts +23 -0
- package/dist/keypad/index.js +63 -0
- package/dist/keypad/model.d.ts +20 -0
- package/dist/keypad/model.js +33 -0
- package/dist/layout/index.d.ts +63 -0
- package/dist/layout/index.js +62 -0
- package/dist/log/index.d.ts +45 -0
- package/dist/log/index.js +159 -0
- package/dist/log/model.d.ts +16 -0
- package/dist/log/model.js +13 -0
- package/dist/node-graph/index.d.ts +123 -0
- package/dist/node-graph/index.js +304 -0
- package/dist/number-ticker/index.d.ts +31 -0
- package/dist/number-ticker/index.js +135 -0
- package/dist/number-ticker/model.d.ts +41 -0
- package/dist/number-ticker/model.js +163 -0
- package/dist/number-ticker/precision.d.ts +7 -0
- package/dist/number-ticker/precision.js +134 -0
- package/dist/overflow-list/index.d.ts +39 -0
- package/dist/overflow-list/index.js +230 -0
- package/dist/overflow-list/layout.d.ts +19 -0
- package/dist/overflow-list/layout.js +44 -0
- package/dist/overlay/Overlay.d.ts +1 -1
- package/dist/overlay/Overlay.js +4 -2
- package/dist/overlay/Popover.d.ts +1 -1
- package/dist/overlay/Popover.js +3 -2
- package/dist/overlay/themeBridge.d.ts +12 -1
- package/dist/overlay/themeBridge.js +27 -1
- package/dist/pagination/index.d.ts +23 -0
- package/dist/pagination/index.js +41 -0
- package/dist/pagination/model.d.ts +4 -0
- package/dist/pagination/model.js +42 -0
- package/dist/pan-zoom/index.d.ts +71 -0
- package/dist/pan-zoom/index.js +175 -0
- package/dist/radar/index.d.ts +56 -0
- package/dist/radar/index.js +220 -0
- package/dist/range-slider/index.d.ts +16 -0
- package/dist/range-slider/index.js +22 -0
- package/dist/rating/index.d.ts +28 -0
- package/dist/rating/index.js +127 -0
- package/dist/resizable/index.d.ts +32 -0
- package/dist/resizable/index.js +50 -0
- package/dist/scroll-area/index.d.ts +36 -0
- package/dist/scroll-area/index.js +44 -0
- package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
- package/dist/segmented-progress/SegmentedProgress.js +42 -0
- package/dist/slider/index.js +1 -1
- package/dist/slot/index.d.ts +24 -0
- package/dist/slot/index.js +29 -0
- package/dist/sortable/index.d.ts +50 -0
- package/dist/sortable/index.js +283 -0
- package/dist/sortable/model.d.ts +30 -0
- package/dist/sortable/model.js +64 -0
- package/dist/sparkline/index.d.ts +19 -0
- package/dist/sparkline/index.js +240 -0
- package/dist/sparkline/model.d.ts +16 -0
- package/dist/sparkline/model.js +28 -0
- package/dist/steps/index.d.ts +16 -0
- package/dist/steps/index.js +37 -0
- package/dist/styles.css +4957 -269
- package/dist/tag-input/index.d.ts +69 -0
- package/dist/tag-input/index.js +325 -0
- package/dist/tag-input/model.d.ts +14 -0
- package/dist/tag-input/model.js +38 -0
- package/dist/time-picker/index.d.ts +15 -0
- package/dist/time-picker/index.js +168 -0
- package/dist/timeline/index.d.ts +14 -0
- package/dist/timeline/index.js +16 -0
- package/dist/toast/index.d.ts +27 -0
- package/dist/toast/index.js +36 -0
- package/dist/toggle-group/index.d.ts +17 -0
- package/dist/toggle-group/index.js +26 -0
- package/dist/tokens.css +10 -9
- package/dist/tooltip/Tooltip.d.ts +1 -1
- package/dist/tooltip/Tooltip.js +3 -2
- package/dist/transfer/index.d.ts +64 -0
- package/dist/transfer/index.js +174 -0
- package/dist/transfer/model.d.ts +12 -0
- package/dist/transfer/model.js +33 -0
- package/dist/tree/index.d.ts +39 -0
- package/dist/tree/index.js +36 -0
- package/dist/tree-select/index.d.ts +59 -0
- package/dist/tree-select/index.js +142 -0
- package/dist/tree-select/model.d.ts +31 -0
- package/dist/tree-select/model.js +135 -0
- package/dist/typewriter/index.d.ts +21 -0
- package/dist/typewriter/index.js +101 -0
- package/dist/typewriter/model.d.ts +7 -0
- package/dist/typewriter/model.js +45 -0
- package/dist/upload/index.d.ts +59 -0
- package/dist/upload/index.js +230 -0
- package/dist/virtual-list/index.d.ts +44 -0
- package/dist/virtual-list/index.js +129 -0
- package/dist/virtual-list/model.d.ts +7 -0
- package/dist/virtual-list/model.js +27 -0
- package/package.json +23 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { HTMLAttributes, InputHTMLAttributes } from 'react';
|
|
2
|
+
import type { FieldControlSize, FieldControlStateProps } from '../form/types.js';
|
|
3
|
+
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, hasTagValue, normalizeTagInputValue, splitTagInputValue } from './model.js';
|
|
4
|
+
import type { TagInputIdentity } from './model.js';
|
|
5
|
+
export interface TagInputTag extends TagInputIdentity {
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
export type TagInputVariant = 'underline' | 'outline' | 'soft';
|
|
9
|
+
export type TagInputCommitReason = 'enter' | 'delimiter' | 'paste';
|
|
10
|
+
export type TagInputChangeReason = 'add' | 'remove' | 'clear';
|
|
11
|
+
export type TagInputValidationCode = 'duplicate' | 'max-tags' | 'invalid';
|
|
12
|
+
export type TagInputValidationResult = boolean | string | null | undefined;
|
|
13
|
+
export interface TagInputChangeDetails {
|
|
14
|
+
reason: TagInputChangeReason;
|
|
15
|
+
tags: readonly TagInputTag[];
|
|
16
|
+
inputReason?: TagInputCommitReason;
|
|
17
|
+
}
|
|
18
|
+
export interface TagInputDraftChangeDetails {
|
|
19
|
+
reason: 'input' | 'commit' | 'clear';
|
|
20
|
+
}
|
|
21
|
+
export interface TagInputValidationContext {
|
|
22
|
+
tags: readonly TagInputTag[];
|
|
23
|
+
reason: TagInputCommitReason;
|
|
24
|
+
}
|
|
25
|
+
export interface TagInputValidationDetails extends TagInputValidationContext {
|
|
26
|
+
candidate: string;
|
|
27
|
+
code: TagInputValidationCode;
|
|
28
|
+
}
|
|
29
|
+
export interface TagInputLabels {
|
|
30
|
+
tagList: string;
|
|
31
|
+
removeTag: (tag: TagInputTag) => string;
|
|
32
|
+
clearAll: string;
|
|
33
|
+
duplicate: (candidate: string) => string;
|
|
34
|
+
maxTags: (maximum: number) => string;
|
|
35
|
+
invalid: (candidate: string) => string;
|
|
36
|
+
}
|
|
37
|
+
export type TagInputClassNames = Partial<Record<'root' | 'control' | 'list' | 'tag' | 'input' | 'clear' | 'message', string>>;
|
|
38
|
+
export interface TagInputCreateContext extends TagInputValidationContext {
|
|
39
|
+
}
|
|
40
|
+
export interface TagInputProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'defaultValue' | 'onChange'>, FieldControlStateProps {
|
|
41
|
+
value?: readonly TagInputTag[];
|
|
42
|
+
defaultValue?: readonly TagInputTag[];
|
|
43
|
+
onValueChange?: (value: readonly TagInputTag[], details: TagInputChangeDetails) => void;
|
|
44
|
+
inputValue?: string;
|
|
45
|
+
defaultInputValue?: string;
|
|
46
|
+
onInputValueChange?: (value: string, details: TagInputDraftChangeDetails) => void;
|
|
47
|
+
createTag?: (candidate: string, context: TagInputCreateContext) => TagInputTag | null | undefined;
|
|
48
|
+
validateTag?: (candidate: string, context: TagInputValidationContext) => TagInputValidationResult;
|
|
49
|
+
onValidationError?: (message: string, details: TagInputValidationDetails) => void;
|
|
50
|
+
validationMessage?: string;
|
|
51
|
+
getTagValueKey?: (value: string) => string;
|
|
52
|
+
delimiters?: readonly string[];
|
|
53
|
+
maxTags?: number;
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
readOnly?: boolean;
|
|
56
|
+
required?: boolean;
|
|
57
|
+
name?: string;
|
|
58
|
+
form?: string;
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
clearable?: boolean;
|
|
61
|
+
fullWidth?: boolean;
|
|
62
|
+
variant?: TagInputVariant;
|
|
63
|
+
labels?: Partial<TagInputLabels>;
|
|
64
|
+
inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, 'defaultValue' | 'disabled' | 'form' | 'id' | 'name' | 'placeholder' | 'readOnly' | 'required' | 'size' | 'value'>;
|
|
65
|
+
classNames?: TagInputClassNames;
|
|
66
|
+
}
|
|
67
|
+
export declare const TagInput: import("react").ForwardRefExoticComponent<TagInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
68
|
+
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, hasTagValue, normalizeTagInputValue, splitTagInputValue, };
|
|
69
|
+
export type { FieldControlSize, TagInputIdentity };
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
4
|
+
import { PencilLine } from '../PencilLine.js';
|
|
5
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
6
|
+
import { PencilText } from '../PencilText.js';
|
|
7
|
+
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
8
|
+
import { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, hasTagValue, normalizeTagInputValue, splitTagInputValue, } from './model.js';
|
|
9
|
+
const removePaths = [{ d: 'M18 6 6 18' }, { d: 'm6 6 12 12' }];
|
|
10
|
+
const clearPaths = [{ d: 'M4 7h16' }, { d: 'M9 7V4h6v3' }, { d: 'm7 7 1 13h8l1-13' }];
|
|
11
|
+
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
12
|
+
const defaultLabels = {
|
|
13
|
+
tagList: 'Selected tags',
|
|
14
|
+
removeTag: tag => `Remove ${tag.label ?? tag.value}`,
|
|
15
|
+
clearAll: 'Clear all tags',
|
|
16
|
+
duplicate: candidate => `${candidate} is already added.`,
|
|
17
|
+
maxTags: maximum => `You can add up to ${maximum} tags.`,
|
|
18
|
+
invalid: candidate => `${candidate} is not a valid tag.`,
|
|
19
|
+
};
|
|
20
|
+
function joinAriaIds(...values) {
|
|
21
|
+
const ids = values.flatMap(value => value?.split(/\s+/).filter(Boolean) ?? []);
|
|
22
|
+
return [...new Set(ids)].join(' ') || undefined;
|
|
23
|
+
}
|
|
24
|
+
function resolvedLabels(labels) {
|
|
25
|
+
return {
|
|
26
|
+
tagList: labels?.tagList ?? defaultLabels.tagList,
|
|
27
|
+
removeTag: labels?.removeTag ?? defaultLabels.removeTag,
|
|
28
|
+
clearAll: labels?.clearAll ?? defaultLabels.clearAll,
|
|
29
|
+
duplicate: labels?.duplicate ?? defaultLabels.duplicate,
|
|
30
|
+
maxTags: labels?.maxTags ?? defaultLabels.maxTags,
|
|
31
|
+
invalid: labels?.invalid ?? defaultLabels.invalid,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function validationText(result, fallback) {
|
|
35
|
+
if (typeof result === 'string')
|
|
36
|
+
return result || fallback;
|
|
37
|
+
if (result === false)
|
|
38
|
+
return fallback;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
export const TagInput = forwardRef(function TagInput({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, classNames = {}, clearable = true, createTag, defaultInputValue = '', defaultValue = [], delimiters = defaultTagInputDelimiters, disabled, form, fullWidth = true, getTagValueKey = defaultTagValueKey, id, inputProps = {}, inputValue, invalid = false, labels, maxTags, name, onClick, onInputValueChange, onValidationError, onValueChange, placeholder, readOnly, required, seed, size = 'md', validationMessage, validateTag, value, variant = 'outline', ...rootProps }, forwardedRef) {
|
|
42
|
+
const generatedId = useId();
|
|
43
|
+
const inputRef = useRef(null);
|
|
44
|
+
const removeRefs = useRef(new Map());
|
|
45
|
+
const composingRef = useRef(false);
|
|
46
|
+
const defaultTagsRef = useRef(defaultValue);
|
|
47
|
+
const defaultDraftRef = useRef(defaultInputValue);
|
|
48
|
+
const [uncontrolledTags, setUncontrolledTags] = useState(() => [...defaultValue]);
|
|
49
|
+
const [uncontrolledDraft, setUncontrolledDraft] = useState(defaultInputValue);
|
|
50
|
+
const [localValidationMessage, setLocalValidationMessage] = useState();
|
|
51
|
+
const [focusWithin, setFocusWithin] = useState(false);
|
|
52
|
+
const [pendingFocus, setPendingFocus] = useState(null);
|
|
53
|
+
const tagsControlled = value !== undefined;
|
|
54
|
+
const draftControlled = inputValue !== undefined;
|
|
55
|
+
const currentTags = tagsControlled ? value : uncontrolledTags;
|
|
56
|
+
const currentDraft = draftControlled ? inputValue : uncontrolledDraft;
|
|
57
|
+
const copy = useMemo(() => resolvedLabels(labels), [labels]);
|
|
58
|
+
const activeValidationMessage = validationMessage ?? localValidationMessage;
|
|
59
|
+
const messageId = activeValidationMessage ? `inkra-tag-input-${generatedId}-message` : undefined;
|
|
60
|
+
const state = useResolvedControlProps({
|
|
61
|
+
id,
|
|
62
|
+
required,
|
|
63
|
+
disabled,
|
|
64
|
+
readOnly,
|
|
65
|
+
invalid: invalid || Boolean(activeValidationMessage),
|
|
66
|
+
'aria-describedby': joinAriaIds(ariaDescribedBy, messageId),
|
|
67
|
+
'aria-errormessage': activeValidationMessage ? messageId : ariaErrorMessage,
|
|
68
|
+
'aria-invalid': ariaInvalid,
|
|
69
|
+
});
|
|
70
|
+
const resolvedSeed = seed ?? `inkra-tag-input-${state.id ?? generatedId}`;
|
|
71
|
+
const limit = typeof maxTags === 'number' && Number.isFinite(maxTags)
|
|
72
|
+
? Math.max(0, Math.floor(maxTags))
|
|
73
|
+
: Number.POSITIVE_INFINITY;
|
|
74
|
+
const atLimit = currentTags.length >= limit;
|
|
75
|
+
const canEdit = !state.disabled && !state.readOnly;
|
|
76
|
+
const surfaceVariant = state.invalid
|
|
77
|
+
? 'danger'
|
|
78
|
+
: variant === 'soft'
|
|
79
|
+
? 'quiet'
|
|
80
|
+
: variant === 'underline'
|
|
81
|
+
? 'ghost'
|
|
82
|
+
: 'outline';
|
|
83
|
+
defaultTagsRef.current = defaultValue;
|
|
84
|
+
defaultDraftRef.current = defaultInputValue;
|
|
85
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!pendingFocus)
|
|
88
|
+
return;
|
|
89
|
+
const target = 'input' in pendingFocus
|
|
90
|
+
? inputRef.current
|
|
91
|
+
: removeRefs.current.get(pendingFocus.tagId) ?? inputRef.current;
|
|
92
|
+
target?.focus({ preventScroll: true });
|
|
93
|
+
setPendingFocus(null);
|
|
94
|
+
}, [currentTags, pendingFocus]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const input = inputRef.current;
|
|
97
|
+
const ownerForm = input?.form;
|
|
98
|
+
if (!input || !ownerForm || (tagsControlled && draftControlled))
|
|
99
|
+
return;
|
|
100
|
+
let timer;
|
|
101
|
+
const reset = (event) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
timer = setTimeout(() => {
|
|
104
|
+
if (event.defaultPrevented)
|
|
105
|
+
return;
|
|
106
|
+
if (!tagsControlled)
|
|
107
|
+
setUncontrolledTags([...defaultTagsRef.current]);
|
|
108
|
+
if (!draftControlled)
|
|
109
|
+
setUncontrolledDraft(defaultDraftRef.current);
|
|
110
|
+
setLocalValidationMessage(undefined);
|
|
111
|
+
}, 0);
|
|
112
|
+
};
|
|
113
|
+
ownerForm.addEventListener('reset', reset);
|
|
114
|
+
return () => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
ownerForm.removeEventListener('reset', reset);
|
|
117
|
+
};
|
|
118
|
+
}, [draftControlled, form, tagsControlled]);
|
|
119
|
+
const updateDraft = (next, details) => {
|
|
120
|
+
if (!draftControlled)
|
|
121
|
+
setUncontrolledDraft(next);
|
|
122
|
+
onInputValueChange?.(next, details);
|
|
123
|
+
};
|
|
124
|
+
const commitTags = (next, details) => {
|
|
125
|
+
if (!tagsControlled)
|
|
126
|
+
setUncontrolledTags([...next]);
|
|
127
|
+
onValueChange?.(next, details);
|
|
128
|
+
};
|
|
129
|
+
const reportValidation = (candidate, reason, code, message, tags) => {
|
|
130
|
+
setLocalValidationMessage(message);
|
|
131
|
+
onValidationError?.(message, { candidate, code, reason, tags });
|
|
132
|
+
};
|
|
133
|
+
const commitCandidates = (candidates, reason) => {
|
|
134
|
+
if (!canEdit)
|
|
135
|
+
return false;
|
|
136
|
+
const working = [...currentTags];
|
|
137
|
+
const added = [];
|
|
138
|
+
let rejected;
|
|
139
|
+
for (const rawCandidate of candidates) {
|
|
140
|
+
const candidate = normalizeTagInputValue(rawCandidate);
|
|
141
|
+
if (!candidate)
|
|
142
|
+
continue;
|
|
143
|
+
if (working.length >= limit) {
|
|
144
|
+
rejected = { candidate, code: 'max-tags', message: copy.maxTags(limit) };
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (hasTagValue(working, candidate, getTagValueKey)) {
|
|
148
|
+
rejected = { candidate, code: 'duplicate', message: copy.duplicate(candidate) };
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
const context = { tags: working, reason };
|
|
152
|
+
const message = validationText(validateTag?.(candidate, context), copy.invalid(candidate));
|
|
153
|
+
if (message) {
|
|
154
|
+
rejected = { candidate, code: 'invalid', message };
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
const tag = createTag ? createTag(candidate, context) : createDefaultTag(candidate);
|
|
158
|
+
if (!tag || !tag.id || !normalizeTagInputValue(tag.value)) {
|
|
159
|
+
rejected = { candidate, code: 'invalid', message: copy.invalid(candidate) };
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
if (working.some(current => current.id === tag.id) || hasTagValue(working, tag.value, getTagValueKey)) {
|
|
163
|
+
rejected = { candidate, code: 'duplicate', message: copy.duplicate(candidate) };
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
working.push(tag);
|
|
167
|
+
added.push(tag);
|
|
168
|
+
}
|
|
169
|
+
if (added.length > 0)
|
|
170
|
+
commitTags(working, { reason: 'add', tags: added, inputReason: reason });
|
|
171
|
+
if (rejected) {
|
|
172
|
+
updateDraft(rejected.candidate, { reason: 'commit' });
|
|
173
|
+
reportValidation(rejected.candidate, reason, rejected.code, rejected.message, working);
|
|
174
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
setLocalValidationMessage(undefined);
|
|
178
|
+
updateDraft('', { reason: 'commit' });
|
|
179
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
180
|
+
return added.length > 0;
|
|
181
|
+
};
|
|
182
|
+
const commitDraft = (reason) => {
|
|
183
|
+
const parsed = splitTagInputValue(currentDraft, delimiters);
|
|
184
|
+
return commitCandidates(parsed.parts, reason);
|
|
185
|
+
};
|
|
186
|
+
const removeTag = (index, focus = null) => {
|
|
187
|
+
if (!canEdit)
|
|
188
|
+
return;
|
|
189
|
+
const tag = currentTags[index];
|
|
190
|
+
if (!tag)
|
|
191
|
+
return;
|
|
192
|
+
const next = currentTags.filter((_, tagIndex) => tagIndex !== index);
|
|
193
|
+
const nextFocus = focus ?? (currentTags[index + 1]
|
|
194
|
+
? { tagId: currentTags[index + 1].id }
|
|
195
|
+
: currentTags[index - 1]
|
|
196
|
+
? { tagId: currentTags[index - 1].id }
|
|
197
|
+
: { input: true });
|
|
198
|
+
setLocalValidationMessage(undefined);
|
|
199
|
+
commitTags(next, { reason: 'remove', tags: [tag] });
|
|
200
|
+
setPendingFocus(nextFocus);
|
|
201
|
+
};
|
|
202
|
+
const clearAll = () => {
|
|
203
|
+
if (!canEdit || currentTags.length === 0)
|
|
204
|
+
return;
|
|
205
|
+
const removed = [...currentTags];
|
|
206
|
+
setLocalValidationMessage(undefined);
|
|
207
|
+
commitTags([], { reason: 'clear', tags: removed });
|
|
208
|
+
updateDraft('', { reason: 'clear' });
|
|
209
|
+
setPendingFocus({ input: true });
|
|
210
|
+
};
|
|
211
|
+
const focusRemoveButton = (index) => {
|
|
212
|
+
const tag = currentTags[index];
|
|
213
|
+
if (tag)
|
|
214
|
+
removeRefs.current.get(tag.id)?.focus({ preventScroll: true });
|
|
215
|
+
};
|
|
216
|
+
const handleInputChange = (event) => {
|
|
217
|
+
inputProps.onChange?.(event);
|
|
218
|
+
if (event.defaultPrevented)
|
|
219
|
+
return;
|
|
220
|
+
setLocalValidationMessage(undefined);
|
|
221
|
+
updateDraft(event.currentTarget.value, { reason: 'input' });
|
|
222
|
+
};
|
|
223
|
+
const handleInputPaste = (event) => {
|
|
224
|
+
inputProps.onPaste?.(event);
|
|
225
|
+
if (event.defaultPrevented || !canEdit || composingRef.current)
|
|
226
|
+
return;
|
|
227
|
+
const pasted = event.clipboardData.getData('text');
|
|
228
|
+
const start = event.currentTarget.selectionStart ?? currentDraft.length;
|
|
229
|
+
const end = event.currentTarget.selectionEnd ?? start;
|
|
230
|
+
const combined = `${currentDraft.slice(0, start)}${pasted}${currentDraft.slice(end)}`;
|
|
231
|
+
const parsed = splitTagInputValue(combined, delimiters);
|
|
232
|
+
if (!parsed.hasDelimiter)
|
|
233
|
+
return;
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
commitCandidates(parsed.parts, 'paste');
|
|
236
|
+
};
|
|
237
|
+
const handleInputKeyDown = (event) => {
|
|
238
|
+
inputProps.onKeyDown?.(event);
|
|
239
|
+
if (event.defaultPrevented || !canEdit)
|
|
240
|
+
return;
|
|
241
|
+
const nativeEvent = event.nativeEvent;
|
|
242
|
+
if (composingRef.current || nativeEvent.isComposing || nativeEvent.keyCode === 229)
|
|
243
|
+
return;
|
|
244
|
+
const direction = getComputedStyle(event.currentTarget).direction;
|
|
245
|
+
const previousKey = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
|
|
246
|
+
if (event.key === previousKey && event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0) {
|
|
247
|
+
if (currentTags.length > 0 && !state.readOnly) {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
focusRemoveButton(currentTags.length - 1);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (event.key === 'Backspace' && currentDraft.length === 0 && currentTags.length > 0) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
removeTag(currentTags.length - 1, { input: true });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (event.key === 'Enter') {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
commitDraft('enter');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (delimiters.includes(event.key)) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
commitDraft('delimiter');
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const handleTagKeyDown = (event, index) => {
|
|
269
|
+
if (event.defaultPrevented || !canEdit)
|
|
270
|
+
return;
|
|
271
|
+
const direction = getComputedStyle(event.currentTarget).direction;
|
|
272
|
+
const previousKey = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft';
|
|
273
|
+
const nextKey = direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';
|
|
274
|
+
if (event.key === previousKey) {
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
focusRemoveButton(Math.max(0, index - 1));
|
|
277
|
+
}
|
|
278
|
+
else if (event.key === nextKey) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
if (index >= currentTags.length - 1)
|
|
281
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
282
|
+
else
|
|
283
|
+
focusRemoveButton(index + 1);
|
|
284
|
+
}
|
|
285
|
+
else if (event.key === 'Home') {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
focusRemoveButton(0);
|
|
288
|
+
}
|
|
289
|
+
else if (event.key === 'End') {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
292
|
+
}
|
|
293
|
+
else if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
removeTag(index);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
return (_jsxs("div", { ...rootProps, className: classes('inkra-field-control-hit', 'inkra-tag-input', className, classNames.root), "data-disabled": state.disabled || undefined, "data-focus-within": focusWithin || undefined, "data-full-width": fullWidth, "data-invalid": state.invalid || undefined, "data-max-reached": atLimit || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, "data-variant": variant, onBlurCapture: event => {
|
|
299
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
300
|
+
setFocusWithin(false);
|
|
301
|
+
rootProps.onBlurCapture?.(event);
|
|
302
|
+
}, onClick: event => {
|
|
303
|
+
onClick?.(event);
|
|
304
|
+
if (event.defaultPrevented || state.disabled)
|
|
305
|
+
return;
|
|
306
|
+
const target = event.target;
|
|
307
|
+
if (!target.closest('button, input'))
|
|
308
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
309
|
+
}, onFocusCapture: event => {
|
|
310
|
+
setFocusWithin(true);
|
|
311
|
+
rootProps.onFocusCapture?.(event);
|
|
312
|
+
}, children: [_jsxs(PencilSurface, { as: "div", className: classes('inkra-field-control', 'inkra-tag-input__control', classNames.control), radius: 8, seed: resolvedSeed, variant: surfaceVariant, children: [_jsx("ul", { "aria-label": copy.tagList, className: classes('inkra-tag-input__list', classNames.list), children: currentTags.map((tag, index) => (_jsxs(PencilSurface, { as: "li", className: classes('inkra-tag-input__tag', classNames.tag), clipContent: true, "data-tag-id": tag.id, radius: 6, seed: `${resolvedSeed}-tag-${tag.id}`, variant: "quiet", children: [_jsx("span", { className: "inkra-tag-input__tag-label", children: tag.label ?? tag.value }), !state.readOnly ? (_jsx("button", { "aria-label": copy.removeTag(tag), className: "inkra-tag-input__remove", disabled: state.disabled, onClick: () => removeTag(index), onKeyDown: event => handleTagKeyDown(event, index), ref: node => {
|
|
313
|
+
if (node)
|
|
314
|
+
removeRefs.current.set(tag.id, node);
|
|
315
|
+
else
|
|
316
|
+
removeRefs.current.delete(tag.id);
|
|
317
|
+
}, title: copy.removeTag(tag), type: "button", children: _jsx(PencilIcon, { decorative: true, paths: removePaths, seed: `${resolvedSeed}-remove-${tag.id}`, size: 18, sourceSize: 24 }) })) : null] }, `${tag.id}-${index}`))) }), _jsx("input", { ...inputProps, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-readonly": state.readOnly || undefined, "aria-required": state.required || undefined, autoComplete: inputProps.autoComplete ?? 'off', className: classes('inkra-input', 'inkra-tag-input__input', inputProps.className, classNames.input), disabled: state.disabled, form: form, id: state.id, onChange: handleInputChange, onCompositionEnd: event => {
|
|
318
|
+
composingRef.current = false;
|
|
319
|
+
inputProps.onCompositionEnd?.(event);
|
|
320
|
+
}, onCompositionStart: event => {
|
|
321
|
+
composingRef.current = true;
|
|
322
|
+
inputProps.onCompositionStart?.(event);
|
|
323
|
+
}, onKeyDown: handleInputKeyDown, onPaste: handleInputPaste, placeholder: placeholder, readOnly: state.readOnly, ref: inputRef, required: state.required && currentTags.length === 0, spellCheck: inputProps.spellCheck ?? false, type: "text", value: currentDraft }), clearable && currentTags.length > 0 && !state.readOnly ? (_jsx("button", { "aria-label": copy.clearAll, className: classes('inkra-tag-input__clear', classNames.clear), disabled: state.disabled, onClick: clearAll, onPointerDown: (event) => event.preventDefault(), title: copy.clearAll, type: "button", children: _jsx(PencilIcon, { decorative: true, paths: clearPaths, seed: `${resolvedSeed}-clear`, size: 20, sourceSize: 24 }) })) : null, variant === 'underline' ? (_jsx(PencilLine, { className: "inkra-tag-input__underline", seed: `${resolvedSeed}-underline`, weight: (focusWithin ? 1.5 : 1) + (state.invalid ? 0.1 : 0) })) : null] }), name ? currentTags.map((tag, index) => (_jsx("input", { disabled: state.disabled, form: form, name: name, type: "hidden", value: tag.value }, `${tag.id}-${index}`))) : null, activeValidationMessage ? (_jsx(PencilText, { as: "span", className: classes('inkra-tag-input__message', classNames.message), id: messageId, role: "alert", seed: `${resolvedSeed}-message`, children: activeValidationMessage })) : null] }));
|
|
324
|
+
});
|
|
325
|
+
export { createDefaultTag, defaultTagInputDelimiters, defaultTagValueKey, hasTagValue, normalizeTagInputValue, splitTagInputValue, };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TagInputIdentity {
|
|
2
|
+
id: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SplitTagInputResult {
|
|
6
|
+
parts: string[];
|
|
7
|
+
hasDelimiter: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const defaultTagInputDelimiters: readonly [",", ";", "\n"];
|
|
10
|
+
export declare function normalizeTagInputValue(value: string): string;
|
|
11
|
+
export declare function defaultTagValueKey(value: string): string;
|
|
12
|
+
export declare function splitTagInputValue(value: string, delimiters?: readonly string[]): SplitTagInputResult;
|
|
13
|
+
export declare function hasTagValue(tags: readonly TagInputIdentity[], value: string, getTagValueKey?: (value: string) => string): boolean;
|
|
14
|
+
export declare function createDefaultTag(value: string): TagInputIdentity;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const defaultTagInputDelimiters = [',', ';', '\n'];
|
|
2
|
+
export function normalizeTagInputValue(value) {
|
|
3
|
+
return value.trim();
|
|
4
|
+
}
|
|
5
|
+
export function defaultTagValueKey(value) {
|
|
6
|
+
return normalizeTagInputValue(value).toLocaleLowerCase();
|
|
7
|
+
}
|
|
8
|
+
export function splitTagInputValue(value, delimiters = defaultTagInputDelimiters) {
|
|
9
|
+
const activeDelimiters = [...new Set(delimiters.filter(Boolean))]
|
|
10
|
+
.sort((left, right) => right.length - left.length);
|
|
11
|
+
if (activeDelimiters.length === 0)
|
|
12
|
+
return { parts: [value], hasDelimiter: false };
|
|
13
|
+
const parts = [];
|
|
14
|
+
let start = 0;
|
|
15
|
+
let index = 0;
|
|
16
|
+
let hasDelimiter = false;
|
|
17
|
+
while (index < value.length) {
|
|
18
|
+
const delimiter = activeDelimiters.find(candidate => value.startsWith(candidate, index));
|
|
19
|
+
if (!delimiter) {
|
|
20
|
+
index += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
parts.push(value.slice(start, index));
|
|
24
|
+
index += delimiter.length;
|
|
25
|
+
start = index;
|
|
26
|
+
hasDelimiter = true;
|
|
27
|
+
}
|
|
28
|
+
parts.push(value.slice(start));
|
|
29
|
+
return { parts, hasDelimiter };
|
|
30
|
+
}
|
|
31
|
+
export function hasTagValue(tags, value, getTagValueKey = defaultTagValueKey) {
|
|
32
|
+
const candidateKey = getTagValueKey(value);
|
|
33
|
+
return tags.some(tag => getTagValueKey(tag.value) === candidateKey);
|
|
34
|
+
}
|
|
35
|
+
export function createDefaultTag(value) {
|
|
36
|
+
const normalized = normalizeTagInputValue(value);
|
|
37
|
+
return { id: normalized, value: normalized };
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MouseEvent, ReactNode } from 'react';
|
|
2
|
+
import type { InputProps, InputValueChangeDetails } from '../form/Input.js';
|
|
3
|
+
export type TimePickerHourCycle = 'h12' | 'h24';
|
|
4
|
+
export type TimePickerChangeDetails = InputValueChangeDetails;
|
|
5
|
+
export interface TimePickerProps extends Omit<InputProps, 'clearable' | 'clearButtonLabel' | 'onClear' | 'onValueChange' | 'type'> {
|
|
6
|
+
hourCycle?: TimePickerHourCycle;
|
|
7
|
+
locale?: string;
|
|
8
|
+
showSeconds?: boolean;
|
|
9
|
+
clearable?: boolean;
|
|
10
|
+
clearButtonLabel?: string;
|
|
11
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
12
|
+
onValueChange?: (value: string, details: TimePickerChangeDetails) => void;
|
|
13
|
+
startIcon?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export declare const TimePicker: import("react").ForwardRefExoticComponent<TimePickerProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
5
|
+
import { mergeAriaIds, useFieldContext } from '../form/context.js';
|
|
6
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
7
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
8
|
+
import { PencilText } from '../PencilText.js';
|
|
9
|
+
import { PencilLine } from '../PencilLine.js';
|
|
10
|
+
import { Popover } from '../overlay/Popover.js';
|
|
11
|
+
import { RadioGroup, RadioGroupItem } from '../selection/RadioGroup.js';
|
|
12
|
+
const clockPaths = [
|
|
13
|
+
{ d: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z' },
|
|
14
|
+
{ d: 'M12 6v6l4 2' },
|
|
15
|
+
];
|
|
16
|
+
const clearPaths = [{ d: 'M18 6 6 18' }, { d: 'm6 6 12 12' }];
|
|
17
|
+
const pad = (value) => {
|
|
18
|
+
const [whole, fraction] = String(value).split('.');
|
|
19
|
+
return `${whole.padStart(2, '0')}${fraction ? `.${fraction}` : ''}`;
|
|
20
|
+
};
|
|
21
|
+
const times = (length, start = 0) => Array.from({ length }, (_, index) => index + start);
|
|
22
|
+
function partsOf(value) {
|
|
23
|
+
if (!/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?$/.test(value))
|
|
24
|
+
return undefined;
|
|
25
|
+
const [hour, minute, second = 0] = value.split(':').map(Number);
|
|
26
|
+
return [hour, minute, second];
|
|
27
|
+
}
|
|
28
|
+
function TimeColumn({ name, label, value, options, onChange, format = pad }) {
|
|
29
|
+
return _jsx(RadioGroup, { name: name, label: label, value: String(value), variant: "segmented", className: "inkra-time-picker__column", onValueChange: next => onChange(Number(next)), children: options.map(option => _jsx(RadioGroupItem, { value: String(option), label: format(option) }, option)) });
|
|
30
|
+
}
|
|
31
|
+
export const TimePicker = forwardRef(function TimePicker(allProps, forwardedRef) {
|
|
32
|
+
const { value, defaultValue, onValueChange, onChange, onClear, clearable = true, clearButtonLabel, hourCycle = 'h24', locale, lang, showSeconds = false, step, min, max, name, form, id, disabled, readOnly, required, invalid, 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, style, size = 'md', fullWidth = true, startIcon, endIcon, endAction, startAction, prefix, suffix, placeholder, seed, variant = 'outline', textAlign = 'start', loading, classNames, ...nativeProps } = allProps;
|
|
33
|
+
const generatedId = useId();
|
|
34
|
+
const field = useFieldContext();
|
|
35
|
+
const inputRef = useRef(null);
|
|
36
|
+
const triggerRef = useRef(null);
|
|
37
|
+
const contentRef = useRef(null);
|
|
38
|
+
const initialValue = useRef(String(defaultValue ?? value ?? ''));
|
|
39
|
+
const controlled = Object.prototype.hasOwnProperty.call(allProps, 'value');
|
|
40
|
+
const [internalValue, setInternalValue] = useState(() => String(defaultValue ?? ''));
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [draft, setDraft] = useState('00:00');
|
|
43
|
+
const [draftValid, setDraftValid] = useState(true);
|
|
44
|
+
const [constraintInvalid, setConstraintInvalid] = useState(false);
|
|
45
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
46
|
+
const state = useResolvedControlProps({
|
|
47
|
+
id: id ?? `inkra-time-picker-${generatedId}`, disabled, readOnly, required, invalid,
|
|
48
|
+
'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid,
|
|
49
|
+
});
|
|
50
|
+
const currentValue = controlled ? String(value ?? '') : internalValue;
|
|
51
|
+
const secondsVisible = showSeconds || (step !== undefined && step !== 'any' && Number(step) % 60 !== 0);
|
|
52
|
+
const resolvedStep = step ?? (secondsVisible ? 1 : 60);
|
|
53
|
+
const stepBase = String(defaultValue ?? initialValue.current);
|
|
54
|
+
const resolvedLocale = locale ?? lang ?? (hourCycle === 'h12' ? 'en-US' : 'en-GB');
|
|
55
|
+
const zh = resolvedLocale.toLowerCase().startsWith('zh');
|
|
56
|
+
const copy = zh
|
|
57
|
+
? { title: '选择时间', hour: '时', minute: '分', second: '秒', period: '时段', am: '上午', pm: '下午',
|
|
58
|
+
done: '确定', cancel: '取消', clear: '清空时间', invalid: '请选择符合时间范围和步进的时间。' }
|
|
59
|
+
: { title: 'Choose time', hour: 'Hour', minute: 'Minute', second: 'Second', period: 'Period', am: 'AM', pm: 'PM',
|
|
60
|
+
done: 'Apply', cancel: 'Cancel', clear: 'Clear time', invalid: 'Choose a time within the allowed range and step.' };
|
|
61
|
+
const currentParts = partsOf(currentValue);
|
|
62
|
+
const displayValue = currentParts
|
|
63
|
+
? new Intl.DateTimeFormat(resolvedLocale, {
|
|
64
|
+
hour: '2-digit', minute: '2-digit', ...(secondsVisible ? { second: '2-digit' } : {}),
|
|
65
|
+
hourCycle: hourCycle === 'h12' ? 'h12' : 'h23',
|
|
66
|
+
}).format(new Date(2000, 0, 1, ...currentParts))
|
|
67
|
+
: placeholder ?? copy.title;
|
|
68
|
+
const canClear = clearable && Boolean(currentValue) && !state.disabled && !state.readOnly;
|
|
69
|
+
const valueId = field ? `${state.id}-value` : undefined;
|
|
70
|
+
const [hour, minute, second] = partsOf(draft) ?? [0, 0, 0];
|
|
71
|
+
const validDraft = (candidate = draft) => {
|
|
72
|
+
// Native validation owns overnight ranges, fractional steps and step bases.
|
|
73
|
+
const input = document.createElement('input');
|
|
74
|
+
input.type = 'time';
|
|
75
|
+
if (min !== undefined)
|
|
76
|
+
input.min = String(min);
|
|
77
|
+
if (max !== undefined)
|
|
78
|
+
input.max = String(max);
|
|
79
|
+
input.step = String(resolvedStep);
|
|
80
|
+
input.defaultValue = stepBase;
|
|
81
|
+
input.required = true;
|
|
82
|
+
input.value = candidate;
|
|
83
|
+
return input.validity.valid;
|
|
84
|
+
};
|
|
85
|
+
const updateOpen = (next) => {
|
|
86
|
+
if (next && (state.disabled || state.readOnly))
|
|
87
|
+
return;
|
|
88
|
+
if (next) {
|
|
89
|
+
const initial = partsOf(currentValue) ? currentValue : partsOf(String(min)) ? String(min) : '00:00';
|
|
90
|
+
const nextDraft = secondsVisible && initial.length === 5 ? `${initial}:00` : initial;
|
|
91
|
+
setDraft(nextDraft);
|
|
92
|
+
setDraftValid(validDraft(nextDraft));
|
|
93
|
+
}
|
|
94
|
+
setOpen(next);
|
|
95
|
+
};
|
|
96
|
+
const updatePart = (part, next) => {
|
|
97
|
+
const values = [hour, minute, second];
|
|
98
|
+
values[part] = next;
|
|
99
|
+
const nextDraft = values.slice(0, secondsVisible ? 3 : 2).map(pad).join(':');
|
|
100
|
+
setDraft(nextDraft);
|
|
101
|
+
setDraftValid(validDraft(nextDraft));
|
|
102
|
+
};
|
|
103
|
+
const commit = (next, reason) => {
|
|
104
|
+
if (!controlled)
|
|
105
|
+
setInternalValue(next);
|
|
106
|
+
setConstraintInvalid(false);
|
|
107
|
+
onValueChange?.(next, { reason });
|
|
108
|
+
setOpen(false);
|
|
109
|
+
};
|
|
110
|
+
useEffect(() => { setDraftValid(validDraft()); }, [draft, min, max, resolvedStep, stepBase]);
|
|
111
|
+
useBrowserLayoutEffect(() => {
|
|
112
|
+
// React's controlled value attribute must not move the native step base on each edit.
|
|
113
|
+
if (inputRef.current)
|
|
114
|
+
inputRef.current.defaultValue = stepBase;
|
|
115
|
+
}, [currentValue, stepBase]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const input = inputRef.current;
|
|
118
|
+
let timer;
|
|
119
|
+
const reset = (event) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
timer = setTimeout(() => {
|
|
122
|
+
if (event.defaultPrevented)
|
|
123
|
+
return;
|
|
124
|
+
if (!controlled)
|
|
125
|
+
setInternalValue(String(defaultValue ?? ''));
|
|
126
|
+
else if (input)
|
|
127
|
+
input.value = currentValue;
|
|
128
|
+
setConstraintInvalid(false);
|
|
129
|
+
setOpen(false);
|
|
130
|
+
}, 0);
|
|
131
|
+
};
|
|
132
|
+
input?.form?.addEventListener('reset', reset);
|
|
133
|
+
return () => { clearTimeout(timer); input?.form?.removeEventListener('reset', reset); };
|
|
134
|
+
}, [controlled, currentValue, defaultValue, form]);
|
|
135
|
+
useEffect(() => { if (state.disabled || state.readOnly)
|
|
136
|
+
setOpen(false); }, [state.disabled, state.readOnly]);
|
|
137
|
+
return _jsxs(Popover.Root, { open: open, onOpenChange: updateOpen, children: [_jsxs("div", { className: ['inkra-time-picker', classNames?.root, className].filter(Boolean).join(' '), style: style, dir: nativeProps.dir, "data-full-width": fullWidth, "data-invalid": state.invalid || constraintInvalid || undefined, "data-readonly": state.readOnly || undefined, "data-disabled": state.disabled || undefined, children: [startAction, _jsx(Popover.Trigger, { asChild: true, children: _jsxs(Button, { className: ['inkra-time-picker__trigger', classNames?.control].filter(Boolean).join(' '), id: state.id, ref: triggerRef, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": mergeAriaIds(state.describedBy, valueId), "aria-invalid": constraintInvalid || state.ariaInvalid, "aria-errormessage": state.errorMessage, "aria-required": state.required || undefined, "aria-readonly": state.readOnly || undefined, disabled: state.disabled, fullWidth: true, align: textAlign, variant: variant === 'soft' ? 'secondary' : variant === 'underline' ? 'outline' : variant, size: size, seed: seed, loading: loading, autoFocus: nativeProps.autoFocus, startIcon: startIcon ?? _jsx(PencilIcon, { paths: clockPaths, sourceSize: 24, size: 22 }), endIcon: endIcon, children: [prefix, _jsx(PencilText, { children: displayValue }), suffix] }) }), valueId && _jsx("span", { className: "inkra-time-picker__accessible-value", id: valueId, children: displayValue }), clearable && _jsx(Button, { className: "inkra-time-picker__clear", "aria-label": clearButtonLabel ?? copy.clear, title: clearButtonLabel ?? copy.clear, "aria-hidden": !canClear || undefined, disabled: !canClear, tabIndex: canClear ? 0 : -1, variant: "outline", size: "icon", onClick: event => {
|
|
138
|
+
commit('', 'clear');
|
|
139
|
+
onClear?.(event);
|
|
140
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
141
|
+
}, children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 22 }) }), endAction, _jsx("input", { ...nativeProps, ref: inputRef, type: "time", className: "inkra-time-picker__native", id: `${state.id}-native`, "aria-hidden": "true", tabIndex: -1, name: name, form: form, value: currentValue, min: min, max: max, step: resolvedStep, lang: lang ?? resolvedLocale, "data-hour-cycle": hourCycle, disabled: state.disabled, readOnly: state.readOnly, required: state.required, onChange: event => {
|
|
142
|
+
if (!controlled)
|
|
143
|
+
setInternalValue(event.currentTarget.value);
|
|
144
|
+
onChange?.(event);
|
|
145
|
+
onValueChange?.(event.currentTarget.value, { reason: 'input' });
|
|
146
|
+
}, onFocus: event => { nativeProps.onFocus?.(event); triggerRef.current?.focus({ preventScroll: true }); }, onInvalid: event => {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
setConstraintInvalid(true);
|
|
149
|
+
triggerRef.current?.focus();
|
|
150
|
+
nativeProps.onInvalid?.(event);
|
|
151
|
+
} })] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { className: "inkra-time-picker__popover", ref: contentRef, align: "start", sideOffset: 8, collisionPadding: 12, "aria-label": copy.title, onOpenAutoFocus: event => {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
requestAnimationFrame(() => {
|
|
154
|
+
const content = contentRef.current;
|
|
155
|
+
content?.querySelectorAll('input:checked').forEach(input => {
|
|
156
|
+
const scroll = input.closest('.inkra-radio-group__items');
|
|
157
|
+
const item = input.closest('.inkra-radio-group__item');
|
|
158
|
+
if (scroll && item)
|
|
159
|
+
scroll.scrollTop += item.getBoundingClientRect().top - scroll.getBoundingClientRect().top - 88;
|
|
160
|
+
});
|
|
161
|
+
(content?.querySelector('input:checked')
|
|
162
|
+
?? content?.querySelector('input'))?.focus({ preventScroll: true });
|
|
163
|
+
});
|
|
164
|
+
}, children: [_jsx(Popover.Title, { children: copy.title }), _jsx(PencilLine, {}), _jsxs("div", { className: "inkra-time-picker__columns", children: [_jsx(TimeColumn, { name: `${generatedId}-hour`, label: copy.hour, value: hourCycle === 'h12' ? hour % 12 || 12 : hour, options: hourCycle === 'h12' ? times(12, 1) : times(24), onChange: next => updatePart(0, hourCycle === 'h12' ? next % 12 + (hour >= 12 ? 12 : 0) : next) }), _jsx(TimeColumn, { name: `${generatedId}-minute`, label: copy.minute, value: minute, options: times(60), onChange: next => updatePart(1, next) }), secondsVisible && _jsx(TimeColumn, { name: `${generatedId}-second`, label: copy.second, value: Math.floor(second), options: times(60), onChange: next => updatePart(2, next) }), hourCycle === 'h12' && _jsx(TimeColumn, { name: `${generatedId}-period`, label: copy.period, value: hour >= 12 ? 1 : 0, options: [0, 1], format: next => next ? copy.pm : copy.am, onChange: next => updatePart(0, hour % 12 + next * 12) })] }), !draftValid && _jsx(PencilText, { as: "p", className: "inkra-time-picker__error", role: "status", children: copy.invalid }), _jsx(PencilLine, {}), _jsxs("div", { className: "inkra-time-picker__footer", children: [_jsx(Popover.Close, { asChild: true, children: _jsx(Button, { variant: "outline", children: copy.cancel }) }), _jsx(Button, { disabled: !draftValid || state.disabled || state.readOnly, onClick: () => {
|
|
165
|
+
if (validDraft())
|
|
166
|
+
commit(draft, 'input');
|
|
167
|
+
}, children: copy.done })] })] }) })] });
|
|
168
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type OlHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
export interface TimelineItem {
|
|
3
|
+
id: string;
|
|
4
|
+
date: ReactNode;
|
|
5
|
+
dateTime?: string;
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
content?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export interface TimelineProps extends Omit<OlHTMLAttributes<HTMLOListElement>, 'children'> {
|
|
11
|
+
items: readonly TimelineItem[];
|
|
12
|
+
orientation?: 'horizontal' | 'vertical';
|
|
13
|
+
}
|
|
14
|
+
export declare const Timeline: import("react").ForwardRefExoticComponent<TimelineProps & import("react").RefAttributes<HTMLOListElement>>;
|