@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;
@@ -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
- let { class: className, ...props }: InputProps = $props();
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
- {...props}
15
- class={cn(
16
- `bg-depth-2 sunken placeholder:text-muted-foreground hover:bg-depth-1 focus:ring-border h-8 w-full rounded-xs border px-2 text-sm shadow-xs transition-all ease-out outline-none focus:ring focus:ring-offset-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus:data-[invalid=true]:ring-red-500`,
17
- className
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-kit/svelte-components",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",