@human-kit/svelte-components 1.0.0-alpha.7 → 1.0.0-alpha.8
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.
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Input
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
`Input` is a headless native text input with modality-aware focus state, RAC-style disabled and read-only booleans, and data attributes for validation and form styling.
|
|
6
|
+
|
|
7
|
+
## Anatomy
|
|
8
|
+
|
|
9
|
+
- `Input`
|
|
10
|
+
|
|
11
|
+
```svelte
|
|
12
|
+
<Input aria-label="Email" placeholder="name@example.com" isInvalid={hasError} isRequired />
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage guidelines
|
|
16
|
+
|
|
17
|
+
- Use native input props like `type`, `name`, `value`, `defaultValue`, `placeholder`, and `autocomplete` directly on `Input`.
|
|
18
|
+
- Prefer `isDisabled`, `isReadOnly`, `isInvalid`, and `isRequired` when you want RAC-style naming while keeping native behavior.
|
|
19
|
+
- Style state with `data-focused`, `data-focus-visible`, `data-hovered`, `data-disabled`, `data-readonly`, `data-invalid`, and `data-required`.
|
|
20
|
+
|
|
21
|
+
## API reference
|
|
22
|
+
|
|
23
|
+
`Input` supports:
|
|
24
|
+
|
|
25
|
+
- `isDisabled?: boolean`
|
|
26
|
+
- `isReadOnly?: boolean`
|
|
27
|
+
- `isInvalid?: boolean`
|
|
28
|
+
- `isRequired?: boolean`
|
|
29
|
+
- `element?: HTMLInputElement | null`
|
|
30
|
+
- `...restProps: HTMLInputAttributes`
|
|
31
|
+
|
|
32
|
+
## Accessibility
|
|
33
|
+
|
|
34
|
+
- `Input` renders a native `<input>` with `type="text"` by default.
|
|
35
|
+
- `data-focus-visible` follows the shared modality contract and only appears for keyboard or virtual focus.
|
|
36
|
+
- `isInvalid` maps to `aria-invalid`, `isReadOnly` maps to `readonly` and `aria-readonly`, and `isRequired` maps to `required` and `aria-required`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Input TODO
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Track Input work with the repository TODO format.
|
|
6
|
+
|
|
7
|
+
## Backlog
|
|
8
|
+
|
|
9
|
+
- [x] [S][P0][Area: Interaction][Owner: Unassigned][Target: Done] Expose modality-aware focus and hover state on the native input.
|
|
10
|
+
- [x] [S][P0][Area: Accessibility][Owner: Unassigned][Target: Done] Support RAC-style `isDisabled`, `isReadOnly`, `isInvalid`, and `isRequired` props.
|
|
11
|
+
- [x] [S][P0][Area: Testing][Owner: Unassigned][Target: Done] Add baseline tests for focus-visible, hover, disabled, read-only, and validation attributes.
|
|
12
|
+
- [ ] [S][P1][Area: API][Owner: Unassigned][Target: TBD] Evaluate whether a composed `TextField` wrapper should own label, description, and error text semantics.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Input from './index';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
isReadOnly?: boolean;
|
|
7
|
+
isInvalid?: boolean;
|
|
8
|
+
isRequired?: boolean;
|
|
9
|
+
onMouseEnter?: (event: MouseEvent) => void;
|
|
10
|
+
onFocus?: (event: FocusEvent) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
isDisabled = false,
|
|
15
|
+
isReadOnly = false,
|
|
16
|
+
isInvalid = false,
|
|
17
|
+
isRequired = false,
|
|
18
|
+
onMouseEnter,
|
|
19
|
+
onFocus
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<form>
|
|
24
|
+
<button type="button">Before</button>
|
|
25
|
+
|
|
26
|
+
<Input
|
|
27
|
+
aria-label="Email"
|
|
28
|
+
placeholder="name@example.com"
|
|
29
|
+
{isDisabled}
|
|
30
|
+
{isReadOnly}
|
|
31
|
+
{isInvalid}
|
|
32
|
+
{isRequired}
|
|
33
|
+
onmouseenter={onMouseEnter}
|
|
34
|
+
onfocus={onFocus}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
<button type="button">After</button>
|
|
38
|
+
</form>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
isDisabled?: boolean;
|
|
3
|
+
isReadOnly?: boolean;
|
|
4
|
+
isInvalid?: boolean;
|
|
5
|
+
isRequired?: boolean;
|
|
6
|
+
onMouseEnter?: (event: MouseEvent) => void;
|
|
7
|
+
onFocus?: (event: FocusEvent) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const InputTest: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type InputTest = ReturnType<typeof InputTest>;
|
|
11
|
+
export default InputTest;
|
package/dist/input/input.svelte
CHANGED
|
@@ -1,19 +1,154 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
2
|
import type { ClassValue } from 'class-variance-authority/types';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
5
|
+
import { shouldShowFocusVisible, trackInteractionModality } from '../primitives/input-modality';
|
|
4
6
|
import { cn } from '../utils/cn';
|
|
5
7
|
|
|
8
|
+
type AriaInvalidValue = HTMLInputAttributes['aria-invalid'];
|
|
9
|
+
|
|
6
10
|
type InputProps = HTMLInputAttributes & {
|
|
7
11
|
class?: ClassValue;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
isReadOnly?: boolean;
|
|
14
|
+
isInvalid?: boolean;
|
|
15
|
+
isRequired?: boolean;
|
|
16
|
+
element?: HTMLInputElement | null;
|
|
8
17
|
};
|
|
9
18
|
|
|
10
|
-
|
|
19
|
+
function composeEventHandlers<TEvent extends Event>(
|
|
20
|
+
internalHandler: ((event: TEvent) => void) | undefined,
|
|
21
|
+
externalHandler: ((event: TEvent) => void) | undefined
|
|
22
|
+
): (event: TEvent) => void {
|
|
23
|
+
return (event: TEvent) => {
|
|
24
|
+
internalHandler?.(event);
|
|
25
|
+
externalHandler?.(event);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isAriaInvalidValue(value: AriaInvalidValue | undefined): boolean {
|
|
30
|
+
return value === true || value === 'true' || value === 'grammar' || value === 'spelling';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const generatedId = $props.id();
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
id,
|
|
37
|
+
type = 'text',
|
|
38
|
+
class: className,
|
|
39
|
+
disabled: disabledProp = false,
|
|
40
|
+
readonly: readOnlyProp = false,
|
|
41
|
+
required: requiredProp = false,
|
|
42
|
+
'aria-invalid': ariaInvalidProp,
|
|
43
|
+
isDisabled = false,
|
|
44
|
+
isReadOnly = false,
|
|
45
|
+
isInvalid = false,
|
|
46
|
+
isRequired = false,
|
|
47
|
+
element = $bindable<HTMLInputElement | null>(null),
|
|
48
|
+
onfocus: onFocusExternal,
|
|
49
|
+
onblur: onBlurExternal,
|
|
50
|
+
onkeydown: onKeyDownExternal,
|
|
51
|
+
onmousedown: onMouseDownExternal,
|
|
52
|
+
onpointerdown: onPointerDownExternal,
|
|
53
|
+
onmouseenter: onMouseEnterExternal,
|
|
54
|
+
onmouseleave: onMouseLeaveExternal,
|
|
55
|
+
...restProps
|
|
56
|
+
}: InputProps = $props();
|
|
57
|
+
|
|
58
|
+
const resolvedId = untrack(() => id) ?? generatedId;
|
|
59
|
+
|
|
60
|
+
let inputRef: HTMLInputElement | null = $state(null);
|
|
61
|
+
let hovered = $state(false);
|
|
62
|
+
let focused = $state(false);
|
|
63
|
+
let focusVisible = $state(false);
|
|
64
|
+
|
|
65
|
+
const resolvedDisabled = $derived(Boolean(isDisabled || disabledProp));
|
|
66
|
+
const resolvedReadOnly = $derived(Boolean(isReadOnly || readOnlyProp));
|
|
67
|
+
const resolvedRequired = $derived(Boolean(isRequired || requiredProp));
|
|
68
|
+
const resolvedInvalid = $derived(Boolean(isInvalid || isAriaInvalidValue(ariaInvalidProp)));
|
|
69
|
+
const renderedAriaInvalid = $derived.by<AriaInvalidValue | undefined>(() => {
|
|
70
|
+
if (!resolvedInvalid) return undefined;
|
|
71
|
+
return ariaInvalidProp === 'grammar' || ariaInvalidProp === 'spelling'
|
|
72
|
+
? ariaInvalidProp
|
|
73
|
+
: 'true';
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
$effect(() => {
|
|
77
|
+
element = inputRef;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
$effect(() => {
|
|
81
|
+
if (!resolvedDisabled) return;
|
|
82
|
+
hovered = false;
|
|
83
|
+
focused = false;
|
|
84
|
+
focusVisible = false;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function handleFocus() {
|
|
88
|
+
if (resolvedDisabled) return;
|
|
89
|
+
focused = true;
|
|
90
|
+
focusVisible = shouldShowFocusVisible(inputRef);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleBlur() {
|
|
94
|
+
focused = false;
|
|
95
|
+
focusVisible = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
99
|
+
trackInteractionModality(event, inputRef);
|
|
100
|
+
focusVisible = focused ? true : shouldShowFocusVisible(inputRef);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleMouseDown(event: MouseEvent) {
|
|
104
|
+
trackInteractionModality(event, inputRef);
|
|
105
|
+
focusVisible = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handlePointerDown(event: PointerEvent) {
|
|
109
|
+
trackInteractionModality(event, inputRef);
|
|
110
|
+
focusVisible = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function handleMouseEnter() {
|
|
114
|
+
if (resolvedDisabled) {
|
|
115
|
+
hovered = false;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
hovered = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function handleMouseLeave() {
|
|
123
|
+
hovered = false;
|
|
124
|
+
}
|
|
11
125
|
</script>
|
|
12
126
|
|
|
13
127
|
<input
|
|
14
|
-
{...
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
128
|
+
{...restProps}
|
|
129
|
+
bind:this={inputRef}
|
|
130
|
+
id={resolvedId}
|
|
131
|
+
{type}
|
|
132
|
+
disabled={resolvedDisabled}
|
|
133
|
+
readonly={resolvedReadOnly}
|
|
134
|
+
required={resolvedRequired}
|
|
135
|
+
aria-invalid={renderedAriaInvalid}
|
|
136
|
+
aria-readonly={resolvedReadOnly || undefined}
|
|
137
|
+
aria-required={resolvedRequired || undefined}
|
|
138
|
+
data-input-root="true"
|
|
139
|
+
data-disabled={resolvedDisabled || undefined}
|
|
140
|
+
data-readonly={resolvedReadOnly || undefined}
|
|
141
|
+
data-invalid={resolvedInvalid || undefined}
|
|
142
|
+
data-required={resolvedRequired || undefined}
|
|
143
|
+
data-hovered={hovered || undefined}
|
|
144
|
+
data-focused={focused || undefined}
|
|
145
|
+
data-focus-visible={focusVisible || undefined}
|
|
146
|
+
onfocus={composeEventHandlers(handleFocus, onFocusExternal ?? undefined)}
|
|
147
|
+
onblur={composeEventHandlers(handleBlur, onBlurExternal ?? undefined)}
|
|
148
|
+
onkeydown={composeEventHandlers(handleKeyDown, onKeyDownExternal ?? undefined)}
|
|
149
|
+
onmousedown={composeEventHandlers(handleMouseDown, onMouseDownExternal ?? undefined)}
|
|
150
|
+
onpointerdown={composeEventHandlers(handlePointerDown, onPointerDownExternal ?? undefined)}
|
|
151
|
+
onmouseenter={composeEventHandlers(handleMouseEnter, onMouseEnterExternal ?? undefined)}
|
|
152
|
+
onmouseleave={composeEventHandlers(handleMouseLeave, onMouseLeaveExternal ?? undefined)}
|
|
153
|
+
class={cn('outline-none', className)}
|
|
19
154
|
/>
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
1
|
import type { ClassValue } from 'class-variance-authority/types';
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
3
|
type InputProps = HTMLInputAttributes & {
|
|
4
4
|
class?: ClassValue;
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
isReadOnly?: boolean;
|
|
7
|
+
isInvalid?: boolean;
|
|
8
|
+
isRequired?: boolean;
|
|
9
|
+
element?: HTMLInputElement | null;
|
|
5
10
|
};
|
|
6
|
-
declare const Input: import("svelte").Component<InputProps, {}, "">;
|
|
11
|
+
declare const Input: import("svelte").Component<InputProps, {}, "element">;
|
|
7
12
|
type Input = ReturnType<typeof Input>;
|
|
8
13
|
export default Input;
|