@human-kit/svelte-components 1.0.0-alpha.7 → 1.0.0-alpha.9

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.
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { Input } from '../../input';
3
4
  import { useComboBoxContext } from '../root/context';
4
5
  import {
5
6
  shouldShowFocusVisible,
@@ -17,11 +18,26 @@
17
18
  class?: string;
18
19
  };
19
20
 
21
+ function composeEventHandlers<TEvent extends Event>(
22
+ internalHandler: ((event: TEvent) => void) | undefined,
23
+ externalHandler: ((event: TEvent) => void) | undefined
24
+ ): (event: TEvent) => void {
25
+ return (event: TEvent) => {
26
+ internalHandler?.(event);
27
+ externalHandler?.(event);
28
+ };
29
+ }
30
+
20
31
  let {
21
32
  'aria-label': ariaLabel,
22
33
  'aria-labelledby': ariaLabelledby,
23
34
  'aria-describedby': ariaDescribedby,
24
35
  class: className,
36
+ oninput: onInputExternal,
37
+ onfocus: onFocusExternal,
38
+ onmousedown: onMouseDownExternal,
39
+ onblur: onBlurExternal,
40
+ onkeydown: onKeyDownExternal,
25
41
  ...restProps
26
42
  }: ComboBoxInputProps = $props();
27
43
 
@@ -83,8 +99,8 @@
83
99
  }
84
100
  </script>
85
101
 
86
- <input
87
- bind:this={inputRef}
102
+ <Input
103
+ bind:element={inputRef}
88
104
  type="text"
89
105
  role="combobox"
90
106
  aria-autocomplete="list"
@@ -98,13 +114,13 @@
98
114
  aria-labelledby={ariaLabelledby}
99
115
  aria-describedby={ariaDescribedby}
100
116
  value={ctx.displayValue}
101
- disabled={ctx.isDisabled}
102
- readonly={ctx.isReadOnly}
103
- oninput={handleInput}
104
- onfocus={handleFocus}
105
- onmousedown={handleMouseDown}
106
- onblur={handleBlur}
107
- onkeydown={handleKeyDown}
117
+ isDisabled={ctx.isDisabled}
118
+ isReadOnly={ctx.isReadOnly}
119
+ oninput={composeEventHandlers(handleInput, onInputExternal ?? undefined)}
120
+ onfocus={composeEventHandlers(handleFocus, onFocusExternal ?? undefined)}
121
+ onmousedown={composeEventHandlers(handleMouseDown, onMouseDownExternal ?? undefined)}
122
+ onblur={composeEventHandlers(handleBlur, onBlurExternal ?? undefined)}
123
+ onkeydown={composeEventHandlers(handleKeyDown, onKeyDownExternal ?? undefined)}
108
124
  class={cn(
109
125
  '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',
110
126
  className
@@ -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.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",