@human-kit/svelte-components 1.0.0-alpha.1 → 1.0.0-alpha.2
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/dist/calendar/README.md +65 -0
- package/dist/calendar/TODO.md +109 -0
- package/dist/calendar/body-cell/calendar-body-cell.svelte +155 -0
- package/dist/calendar/body-cell/calendar-body-cell.svelte.d.ts +9 -0
- package/dist/calendar/grid/calendar-grid-month-scope.svelte +16 -0
- package/dist/calendar/grid/calendar-grid-month-scope.svelte.d.ts +8 -0
- package/dist/calendar/grid/calendar-grid.svelte +45 -0
- package/dist/calendar/grid/calendar-grid.svelte.d.ts +8 -0
- package/dist/calendar/grid/month-scope.d.ts +2 -0
- package/dist/calendar/grid/month-scope.js +8 -0
- package/dist/calendar/grid-body/calendar-grid-body-custom-test.svelte +13 -0
- package/dist/calendar/grid-body/calendar-grid-body-custom-test.svelte.d.ts +18 -0
- package/dist/calendar/grid-body/calendar-grid-body.svelte +36 -0
- package/dist/calendar/grid-body/calendar-grid-body.svelte.d.ts +8 -0
- package/dist/calendar/grid-header/calendar-grid-header-custom-test.svelte +13 -0
- package/dist/calendar/grid-header/calendar-grid-header-custom-test.svelte.d.ts +18 -0
- package/dist/calendar/grid-header/calendar-grid-header.svelte +31 -0
- package/dist/calendar/grid-header/calendar-grid-header.svelte.d.ts +8 -0
- package/dist/calendar/header-cell/calendar-header-cell-test.svelte +11 -0
- package/dist/calendar/header-cell/calendar-header-cell-test.svelte.d.ts +18 -0
- package/dist/calendar/header-cell/calendar-header-cell.svelte +16 -0
- package/dist/calendar/header-cell/calendar-header-cell.svelte.d.ts +8 -0
- package/dist/calendar/heading/calendar-heading.svelte +17 -0
- package/dist/calendar/heading/calendar-heading.svelte.d.ts +5 -0
- package/dist/calendar/index.d.ts +13 -0
- package/dist/calendar/index.js +13 -0
- package/dist/calendar/index.parts.d.ts +9 -0
- package/dist/calendar/index.parts.js +9 -0
- package/dist/calendar/root/calendar-root-bind-value-test.svelte +14 -0
- package/dist/calendar/root/calendar-root-bind-value-test.svelte.d.ts +3 -0
- package/dist/calendar/root/calendar-root-controlled-clear-test.svelte +20 -0
- package/dist/calendar/root/calendar-root-controlled-clear-test.svelte.d.ts +3 -0
- package/dist/calendar/root/calendar-root-test.svelte +67 -0
- package/dist/calendar/root/calendar-root-test.svelte.d.ts +12 -0
- package/dist/calendar/root/calendar-root.svelte +140 -0
- package/dist/calendar/root/calendar-root.svelte.d.ts +30 -0
- package/dist/calendar/root/context.d.ts +62 -0
- package/dist/calendar/root/context.js +724 -0
- package/dist/calendar/root/date-utils.d.ts +17 -0
- package/dist/calendar/root/date-utils.js +104 -0
- package/dist/calendar/trigger-next/calendar-trigger-next.svelte +38 -0
- package/dist/calendar/trigger-next/calendar-trigger-next.svelte.d.ts +8 -0
- package/dist/calendar/trigger-previous/calendar-trigger-previous.svelte +38 -0
- package/dist/calendar/trigger-previous/calendar-trigger-previous.svelte.d.ts +8 -0
- package/dist/combobox/README.md +40 -0
- package/dist/combobox/TODO.md +92 -92
- package/dist/combobox/button/README.md +15 -0
- package/dist/combobox/input/README.md +16 -0
- package/dist/combobox/item/README.md +27 -0
- package/dist/combobox/item-indicator/README.md +15 -0
- package/dist/combobox/list/README.md +27 -0
- package/dist/combobox/popover/README.md +13 -0
- package/dist/combobox/root/README.md +44 -0
- package/dist/combobox/tag/README.md +37 -0
- package/dist/combobox/tag-remove/README.md +14 -0
- package/dist/combobox/tags/README.md +23 -0
- package/dist/dialog/README.md +35 -0
- package/dist/dialog/content/README.md +16 -0
- package/dist/dialog/overlay/README.md +13 -0
- package/dist/dialog/portal/README.md +12 -0
- package/dist/dialog/root/README.md +53 -0
- package/dist/dialog/trigger/README.md +12 -0
- package/dist/dialog/trigger/dialog-trigger-multi-button-test.svelte +19 -0
- package/dist/dialog/trigger/dialog-trigger-multi-button-test.svelte.d.ts +18 -0
- package/dist/dialog/trigger/dialog-trigger.svelte +18 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/listbox/README.md +26 -0
- package/dist/listbox/item/README.md +24 -0
- package/dist/listbox/root/README.md +40 -0
- package/dist/locale-provider/context.d.ts +8 -0
- package/dist/locale-provider/context.js +18 -0
- package/dist/locale-provider/index.d.ts +4 -0
- package/dist/locale-provider/index.js +4 -0
- package/dist/locale-provider/locale-provider-initial-value-test.svelte +15 -0
- package/dist/locale-provider/locale-provider-initial-value-test.svelte.d.ts +7 -0
- package/dist/locale-provider/locale-provider-test.svelte +20 -0
- package/dist/locale-provider/locale-provider-test.svelte.d.ts +6 -0
- package/dist/locale-provider/locale-provider-value-probe.svelte +22 -0
- package/dist/locale-provider/locale-provider-value-probe.svelte.d.ts +6 -0
- package/dist/locale-provider/locale-provider.svelte +23 -0
- package/dist/locale-provider/locale-provider.svelte.d.ts +8 -0
- package/dist/popover/README.md +32 -0
- package/dist/popover/content/README.md +25 -0
- package/dist/popover/root/README.md +30 -0
- package/dist/popover/trigger/README.md +23 -0
- package/dist/popover/trigger/popover-trigger-button.svelte +6 -3
- package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +2 -3
- package/dist/popover/trigger/popover-trigger-multi-button-test.svelte +16 -0
- package/dist/popover/trigger/popover-trigger-multi-button-test.svelte.d.ts +18 -0
- package/dist/popover/trigger/popover-trigger.svelte +18 -6
- package/package.json +11 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type CalendarDateValue = string;
|
|
2
|
+
export declare function isValidCalendarDateValue(value: string): boolean;
|
|
3
|
+
export declare function parseCalendarDate(value: CalendarDateValue): Date | null;
|
|
4
|
+
export declare function formatCalendarDate(date: Date): CalendarDateValue;
|
|
5
|
+
export declare function startOfMonth(date: Date): Date;
|
|
6
|
+
export declare function addMonths(date: Date, amount: number): Date;
|
|
7
|
+
export declare function addDays(date: Date, amount: number): Date;
|
|
8
|
+
export declare function compareDates(a: Date, b: Date): number;
|
|
9
|
+
export declare function getTodayUtcDate(): Date;
|
|
10
|
+
export declare function getFirstDayOfWeek(locale: string): number;
|
|
11
|
+
export declare function getWeekdayLabels(locale: string, firstDayOfWeek: number): string[];
|
|
12
|
+
export type CalendarDayCell = {
|
|
13
|
+
date: CalendarDateValue;
|
|
14
|
+
isOutsideMonth: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare function buildMonthGrid(monthStart: Date, firstDayOfWeek: number): CalendarDayCell[][];
|
|
17
|
+
export declare function formatMonthHeading(date: Date, locale: string): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
2
|
+
export function isValidCalendarDateValue(value) {
|
|
3
|
+
const match = DATE_RE.exec(value);
|
|
4
|
+
if (!match)
|
|
5
|
+
return false;
|
|
6
|
+
const year = Number(match[1]);
|
|
7
|
+
const month = Number(match[2]);
|
|
8
|
+
const day = Number(match[3]);
|
|
9
|
+
if (month < 1 || month > 12 || day < 1 || day > 31)
|
|
10
|
+
return false;
|
|
11
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
12
|
+
return (date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day);
|
|
13
|
+
}
|
|
14
|
+
export function parseCalendarDate(value) {
|
|
15
|
+
if (!isValidCalendarDateValue(value))
|
|
16
|
+
return null;
|
|
17
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
18
|
+
return new Date(Date.UTC(year, month - 1, day));
|
|
19
|
+
}
|
|
20
|
+
export function formatCalendarDate(date) {
|
|
21
|
+
const year = date.getUTCFullYear();
|
|
22
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
23
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
24
|
+
return `${year}-${month}-${day}`;
|
|
25
|
+
}
|
|
26
|
+
export function startOfMonth(date) {
|
|
27
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
|
|
28
|
+
}
|
|
29
|
+
function getDaysInMonthUtc(year, monthIndex) {
|
|
30
|
+
return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
|
|
31
|
+
}
|
|
32
|
+
export function addMonths(date, amount) {
|
|
33
|
+
const targetMonth = date.getUTCMonth() + amount;
|
|
34
|
+
const targetYear = date.getUTCFullYear() + Math.floor(targetMonth / 12);
|
|
35
|
+
const normalizedMonth = ((targetMonth % 12) + 12) % 12;
|
|
36
|
+
const targetDay = Math.min(date.getUTCDate(), getDaysInMonthUtc(targetYear, normalizedMonth));
|
|
37
|
+
return new Date(Date.UTC(targetYear, normalizedMonth, targetDay));
|
|
38
|
+
}
|
|
39
|
+
export function addDays(date, amount) {
|
|
40
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + amount));
|
|
41
|
+
}
|
|
42
|
+
export function compareDates(a, b) {
|
|
43
|
+
const at = a.getTime();
|
|
44
|
+
const bt = b.getTime();
|
|
45
|
+
if (at < bt)
|
|
46
|
+
return -1;
|
|
47
|
+
if (at > bt)
|
|
48
|
+
return 1;
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
export function getTodayUtcDate() {
|
|
52
|
+
const now = new Date();
|
|
53
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
54
|
+
}
|
|
55
|
+
export function getFirstDayOfWeek(locale) {
|
|
56
|
+
try {
|
|
57
|
+
const intlLocale = new Intl.Locale(locale);
|
|
58
|
+
const weekInfo = intlLocale.weekInfo;
|
|
59
|
+
if (weekInfo?.firstDay) {
|
|
60
|
+
return weekInfo.firstDay % 7;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore and fallback to sunday
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
export function getWeekdayLabels(locale, firstDayOfWeek) {
|
|
69
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
70
|
+
weekday: 'short',
|
|
71
|
+
timeZone: 'UTC'
|
|
72
|
+
});
|
|
73
|
+
const sunday = new Date(Date.UTC(2024, 0, 7));
|
|
74
|
+
return Array.from({ length: 7 }, (_, index) => {
|
|
75
|
+
const dayOffset = (firstDayOfWeek + index) % 7;
|
|
76
|
+
return formatter.format(addDays(sunday, dayOffset));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export function buildMonthGrid(monthStart, firstDayOfWeek) {
|
|
80
|
+
const firstOfMonth = startOfMonth(monthStart);
|
|
81
|
+
const firstWeekday = firstOfMonth.getUTCDay();
|
|
82
|
+
const startOffset = (firstWeekday - firstDayOfWeek + 7) % 7;
|
|
83
|
+
const gridStart = addDays(firstOfMonth, -startOffset);
|
|
84
|
+
const weeks = [];
|
|
85
|
+
for (let weekIndex = 0; weekIndex < 6; weekIndex++) {
|
|
86
|
+
const week = [];
|
|
87
|
+
for (let dayIndex = 0; dayIndex < 7; dayIndex++) {
|
|
88
|
+
const date = addDays(gridStart, weekIndex * 7 + dayIndex);
|
|
89
|
+
week.push({
|
|
90
|
+
date: formatCalendarDate(date),
|
|
91
|
+
isOutsideMonth: date.getUTCMonth() !== firstOfMonth.getUTCMonth()
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
weeks.push(week);
|
|
95
|
+
}
|
|
96
|
+
return weeks;
|
|
97
|
+
}
|
|
98
|
+
export function formatMonthHeading(date, locale) {
|
|
99
|
+
return new Intl.DateTimeFormat(locale, {
|
|
100
|
+
month: 'long',
|
|
101
|
+
year: 'numeric',
|
|
102
|
+
timeZone: 'UTC'
|
|
103
|
+
}).format(date);
|
|
104
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { useCalendarContext } from '../root/context';
|
|
5
|
+
|
|
6
|
+
type CalendarTriggerNextProps = Omit<HTMLButtonAttributes, 'children'> & {
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let { children, class: className = '', ...restProps }: CalendarTriggerNextProps = $props();
|
|
11
|
+
|
|
12
|
+
const calendar = useCalendarContext();
|
|
13
|
+
const layoutVersion = calendar.layoutVersion;
|
|
14
|
+
const isDisabled = $derived.by(() => {
|
|
15
|
+
void $layoutVersion;
|
|
16
|
+
return calendar.isDisabled;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function handleClick() {
|
|
20
|
+
if (isDisabled) return;
|
|
21
|
+
calendar.goToNextPage();
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class={className}
|
|
28
|
+
aria-label="Next page"
|
|
29
|
+
disabled={isDisabled}
|
|
30
|
+
onclick={handleClick}
|
|
31
|
+
{...restProps}
|
|
32
|
+
>
|
|
33
|
+
{#if children}
|
|
34
|
+
{@render children()}
|
|
35
|
+
{:else}
|
|
36
|
+
Next
|
|
37
|
+
{/if}
|
|
38
|
+
</button>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type CalendarTriggerNextProps = Omit<HTMLButtonAttributes, 'children'> & {
|
|
4
|
+
children?: Snippet;
|
|
5
|
+
};
|
|
6
|
+
declare const CalendarTriggerNext: import("svelte").Component<CalendarTriggerNextProps, {}, "">;
|
|
7
|
+
type CalendarTriggerNext = ReturnType<typeof CalendarTriggerNext>;
|
|
8
|
+
export default CalendarTriggerNext;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { useCalendarContext } from '../root/context';
|
|
5
|
+
|
|
6
|
+
type CalendarTriggerPreviousProps = Omit<HTMLButtonAttributes, 'children'> & {
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let { children, class: className = '', ...restProps }: CalendarTriggerPreviousProps = $props();
|
|
11
|
+
|
|
12
|
+
const calendar = useCalendarContext();
|
|
13
|
+
const layoutVersion = calendar.layoutVersion;
|
|
14
|
+
const isDisabled = $derived.by(() => {
|
|
15
|
+
void $layoutVersion;
|
|
16
|
+
return calendar.isDisabled;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function handleClick() {
|
|
20
|
+
if (isDisabled) return;
|
|
21
|
+
calendar.goToPreviousPage();
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class={className}
|
|
28
|
+
aria-label="Previous page"
|
|
29
|
+
disabled={isDisabled}
|
|
30
|
+
onclick={handleClick}
|
|
31
|
+
{...restProps}
|
|
32
|
+
>
|
|
33
|
+
{#if children}
|
|
34
|
+
{@render children()}
|
|
35
|
+
{:else}
|
|
36
|
+
Prev
|
|
37
|
+
{/if}
|
|
38
|
+
</button>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
type CalendarTriggerPreviousProps = Omit<HTMLButtonAttributes, 'children'> & {
|
|
4
|
+
children?: Snippet;
|
|
5
|
+
};
|
|
6
|
+
declare const CalendarTriggerPrevious: import("svelte").Component<CalendarTriggerPreviousProps, {}, "">;
|
|
7
|
+
type CalendarTriggerPrevious = ReturnType<typeof CalendarTriggerPrevious>;
|
|
8
|
+
export default CalendarTriggerPrevious;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ComboBox
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
`ComboBox` combines text input, popover, and listbox behavior into a single accessible selection pattern. It supports single and multiple selection, controlled and uncontrolled state, and keyboard-first interaction.
|
|
6
|
+
|
|
7
|
+
## Usage guidelines
|
|
8
|
+
|
|
9
|
+
- Wrap all parts in `ComboBox.Root`.
|
|
10
|
+
- Use controlled props (`value`, `inputValue`, `isOpen`) only when external state management is needed.
|
|
11
|
+
- Provide a stable `id` in SSR environments to keep ARIA ids deterministic.
|
|
12
|
+
- Render `ComboBox.Tags`, `ComboBox.Tag`, and `ComboBox.TagRemove` in multiple mode to expose selected values.
|
|
13
|
+
- Choose `trigger="focus"`, `trigger="input"`, or `trigger="press"` based on your opening behavior requirements.
|
|
14
|
+
|
|
15
|
+
## Anatomy
|
|
16
|
+
|
|
17
|
+
Import the component and compose its parts:
|
|
18
|
+
|
|
19
|
+
```svelte
|
|
20
|
+
<ComboBox.Root>
|
|
21
|
+
<ComboBox.Input />
|
|
22
|
+
<ComboBox.Button />
|
|
23
|
+
<ComboBox.Popover>
|
|
24
|
+
<ComboBox.List>
|
|
25
|
+
<ComboBox.Item id="1">Option 1</ComboBox.Item>
|
|
26
|
+
</ComboBox.List>
|
|
27
|
+
</ComboBox.Popover>
|
|
28
|
+
</ComboBox.Root>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `ComboBox.Root`
|
|
32
|
+
- `ComboBox.Input`
|
|
33
|
+
- `ComboBox.Button`
|
|
34
|
+
- `ComboBox.Popover`
|
|
35
|
+
- `ComboBox.List`
|
|
36
|
+
- `ComboBox.Item`
|
|
37
|
+
- `ComboBox.ItemIndicator`
|
|
38
|
+
- `ComboBox.Tags`
|
|
39
|
+
- `ComboBox.Tag`
|
|
40
|
+
- `ComboBox.TagRemove`
|
package/dist/combobox/TODO.md
CHANGED
|
@@ -4,134 +4,134 @@ Comprehensive review based on: **Accessibility**, **Scalability**, **Performance
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Accessibility
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Completed (Accessibility)
|
|
10
10
|
|
|
11
|
-
- [x] ARIA pattern: `aria-activedescendant`
|
|
12
|
-
- [x] `aria-expanded`, `aria-haspopup`, `aria-controls`
|
|
13
|
-
- [x] `aria-label`
|
|
11
|
+
- [x] ARIA pattern: `aria-activedescendant` for virtual focus
|
|
12
|
+
- [x] `aria-expanded`, `aria-haspopup`, `aria-controls` on input
|
|
13
|
+
- [x] `aria-label` on ListBox
|
|
14
14
|
- [x] `role="combobox"`, `role="listbox"`, `role="option"`
|
|
15
|
-
- [x] `aria-selected`
|
|
16
|
-
- [x] `aria-disabled`
|
|
17
|
-
- [x] Input
|
|
18
|
-
- [x] ListBox
|
|
19
|
-
- [x] Button
|
|
20
|
-
- [x] Wrapper group
|
|
21
|
-
- [x] Input
|
|
15
|
+
- [x] `aria-selected` on selected items
|
|
16
|
+
- [x] `aria-disabled` on disabled items/placeholder
|
|
17
|
+
- [x] Input supports `aria-label` and `aria-labelledby` props
|
|
18
|
+
- [x] ListBox has an ID so `aria-controls` works correctly
|
|
19
|
+
- [x] Button has `aria-controls` pointing to the listbox
|
|
20
|
+
- [x] Wrapper group supports `aria-label` and `aria-labelledby`
|
|
21
|
+
- [x] Input supports `aria-describedby` for usage instructions
|
|
22
22
|
|
|
23
|
-
###
|
|
23
|
+
### Pending (Accessibility)
|
|
24
24
|
|
|
25
|
-
- [ ] **Live regions
|
|
26
|
-
-
|
|
27
|
-
-
|
|
25
|
+
- [ ] **Live regions for result count**
|
|
26
|
+
- Add `<div aria-live="polite">` to announce "{N} results available" while filtering
|
|
27
|
+
- Important for screen readers that cannot see visual updates
|
|
28
28
|
|
|
29
|
-
- [ ] **
|
|
30
|
-
-
|
|
31
|
-
-
|
|
29
|
+
- [ ] **Selection announcement**
|
|
30
|
+
- Announce "Selected item: {label}" when an item is selected
|
|
31
|
+
- Use `aria-live="assertive"` for important changes
|
|
32
32
|
|
|
33
|
-
- [ ] **
|
|
34
|
-
-
|
|
35
|
-
-
|
|
33
|
+
- [ ] **Support for groups (sections)**
|
|
34
|
+
- Implement `role="group"` with `aria-labelledby` for sections
|
|
35
|
+
- Add `ComboBox.Section` component
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Scalability
|
|
40
40
|
|
|
41
|
-
###
|
|
41
|
+
### Completed (Scalability)
|
|
42
42
|
|
|
43
|
-
- [x]
|
|
43
|
+
- [x] Reusable `useVirtualFocus` hook
|
|
44
44
|
- [x] Controlled/uncontrolled mode
|
|
45
|
-
- [x]
|
|
46
|
-
- [x] `emptyPlaceholder`
|
|
45
|
+
- [x] Automatic item filtering
|
|
46
|
+
- [x] Reactive `emptyPlaceholder`
|
|
47
47
|
|
|
48
|
-
###
|
|
48
|
+
### Pending (Scalability)
|
|
49
49
|
|
|
50
|
-
- [ ]
|
|
51
|
-
-
|
|
52
|
-
-
|
|
50
|
+
- [ ] **Customizable `filterFn` prop**
|
|
51
|
+
- Filtering is currently case-insensitive includes
|
|
52
|
+
- Allow: fuzzy search, startsWith, exact match, async search
|
|
53
53
|
|
|
54
54
|
- [ ] **`allowCreate` prop**
|
|
55
|
-
-
|
|
55
|
+
- Allow creating new items when there is no match
|
|
56
56
|
- Callback `onCreate?: (value: string) => void`
|
|
57
57
|
|
|
58
58
|
- [x] **Multiple selection UI**
|
|
59
|
-
- Chips/tags
|
|
60
|
-
- Clear
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
- `ComboBox.ItemIndicator`
|
|
59
|
+
- Chips/tags for selected items: `ComboBox.Tags`, `ComboBox.Tag`, `ComboBox.TagRemove`
|
|
60
|
+
- Clear-all button (available via `clearSelection()` in context)
|
|
61
|
+
- Selected-count indicator (available via `selectedValue.size`)
|
|
62
|
+
- Keyboard tag navigation (ArrowLeft/Right, Delete/Backspace)
|
|
63
|
+
- `ComboBox.ItemIndicator` to show checks for selected items
|
|
64
64
|
|
|
65
65
|
- [ ] **Form integration**
|
|
66
|
-
- `name` prop
|
|
67
|
-
- Hidden input
|
|
68
|
-
-
|
|
66
|
+
- `name` prop for native `<form>`
|
|
67
|
+
- Hidden input with serialized value
|
|
68
|
+
- Validation with `required`, `aria-invalid`
|
|
69
69
|
|
|
70
70
|
- [ ] **Async data support**
|
|
71
71
|
- Props: `isLoading`, `loadingPlaceholder`
|
|
72
|
-
- Callback: `onLoadMore`
|
|
73
|
-
-
|
|
72
|
+
- Callback: `onLoadMore` for infinite scroll
|
|
73
|
+
- Built-in debounce for async search
|
|
74
74
|
|
|
75
75
|
- [ ] **Virtualization**
|
|
76
|
-
-
|
|
77
|
-
-
|
|
76
|
+
- For large lists (>100 items)
|
|
77
|
+
- Integrate with `@tanstack/virtual` or similar
|
|
78
78
|
|
|
79
79
|
---
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## Performance
|
|
82
82
|
|
|
83
|
-
###
|
|
83
|
+
### Completed (Performance)
|
|
84
84
|
|
|
85
|
-
- [x]
|
|
86
|
-
- [x] `untrack()`
|
|
87
|
-
- [x] Subscription pattern
|
|
85
|
+
- [x] DOM query cache with invalidation (`cachedItemOrder`)
|
|
86
|
+
- [x] `untrack()` to avoid infinite effect loops
|
|
87
|
+
- [x] Subscription pattern for reactive `itemCount`
|
|
88
88
|
- [x] Scoped queries via `containerRef`
|
|
89
89
|
|
|
90
|
-
###
|
|
90
|
+
### Pending (Performance)
|
|
91
91
|
|
|
92
|
-
- [ ]
|
|
93
|
-
-
|
|
94
|
-
-
|
|
92
|
+
- [ ] **`isVisible` memoization in ListBoxItem**
|
|
93
|
+
- It is currently recomputed on each render
|
|
94
|
+
- Consider more granular memoization with `$derived`
|
|
95
95
|
|
|
96
96
|
- [ ] **Batch registration**
|
|
97
|
-
- `registerItem`
|
|
98
|
-
-
|
|
97
|
+
- `registerItem` runs once per individual item
|
|
98
|
+
- For large lists, consider batched notifications
|
|
99
99
|
|
|
100
|
-
- [ ] **Lazy itemLabels
|
|
101
|
-
-
|
|
102
|
-
-
|
|
100
|
+
- [ ] **Lazy `itemLabels`**
|
|
101
|
+
- The `itemLabels` map grows with each item
|
|
102
|
+
- Cleanup on unmount is implemented, but consider `WeakMap`
|
|
103
103
|
|
|
104
104
|
- [ ] **Effect cleanup optimizations**
|
|
105
|
-
-
|
|
106
|
-
- `combobox-listboxitem.svelte`
|
|
105
|
+
- Review effects that could be consolidated
|
|
106
|
+
- `combobox-listboxitem.svelte` has 2 effects that might become 1
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
-
##
|
|
110
|
+
## Svelte 5 Runes Best Practices
|
|
111
111
|
|
|
112
|
-
###
|
|
112
|
+
### Completed (Runes)
|
|
113
113
|
|
|
114
|
-
- [x] `$state`
|
|
115
|
-
- [x] `$derived`
|
|
116
|
-
- [x] `$effect`
|
|
117
|
-
- [x] `$bindable`
|
|
118
|
-
- [x] `$props()`
|
|
119
|
-
- [x] `untrack()`
|
|
120
|
-
- [x] `$derived(expression)`
|
|
121
|
-
- [x]
|
|
114
|
+
- [x] `$state` for reactive state
|
|
115
|
+
- [x] `$derived` for computed values
|
|
116
|
+
- [x] `$effect` with cleanup functions
|
|
117
|
+
- [x] `$bindable` for two-way binding
|
|
118
|
+
- [x] `$props()` for destructuring
|
|
119
|
+
- [x] `untrack()` to avoid unnecessary re-runs
|
|
120
|
+
- [x] `$derived(expression)` instead of `$derived(() => ...)` - simplified in `combobox-listboxitem.svelte`
|
|
121
|
+
- [x] Consolidated effects - using 1 `$effect` + `onDestroy` instead of 2 effects
|
|
122
122
|
|
|
123
|
-
###
|
|
123
|
+
### Reviewed - No Changes Required
|
|
124
124
|
|
|
125
|
-
- [x] **`$effect.pre`**:
|
|
126
|
-
- [x] **Context typing**:
|
|
125
|
+
- [x] **`$effect.pre`**: Reviewed - no race conditions requiring it
|
|
126
|
+
- [x] **Context typing**: Single shared type is appropriate - tree-shaking does not apply to context objects
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
##
|
|
130
|
+
## Testing
|
|
131
131
|
|
|
132
|
-
###
|
|
132
|
+
### Completed (Testing)
|
|
133
133
|
|
|
134
|
-
- [x] 291
|
|
134
|
+
- [x] 291 passing unit tests
|
|
135
135
|
- [x] Keyboard navigation tests
|
|
136
136
|
- [x] Selection tests
|
|
137
137
|
- [x] Filtering tests
|
|
@@ -143,33 +143,33 @@ Comprehensive review based on: **Accessibility**, **Scalability**, **Performance
|
|
|
143
143
|
- [x] Selection behavior (Enter, click, Escape restoration)
|
|
144
144
|
- [x] Multi-select tests (12 tests)
|
|
145
145
|
- [x] Tags component tests (4 tests)
|
|
146
|
-
- [x] Tag component tests (13 tests) -
|
|
146
|
+
- [x] Tag component tests (13 tests) - includes keyboard navigation
|
|
147
147
|
- [x] TagRemove component tests (6 tests)
|
|
148
148
|
- [x] ItemIndicator component tests (5 tests)
|
|
149
149
|
|
|
150
|
-
###
|
|
150
|
+
### Pending (Testing)
|
|
151
151
|
|
|
152
|
-
- [ ] **Tests
|
|
153
|
-
- [ ] **Visual regression tests** - screenshots
|
|
152
|
+
- [ ] **Tests with many items (100+)** - performance tests
|
|
153
|
+
- [ ] **Visual regression tests** - state screenshots
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
-
##
|
|
157
|
+
## Documentation
|
|
158
158
|
|
|
159
|
-
- [ ] **JSDoc
|
|
160
|
-
-
|
|
161
|
-
-
|
|
159
|
+
- [ ] **Complete JSDoc**
|
|
160
|
+
- Document all public props
|
|
161
|
+
- Add usage examples in comments
|
|
162
162
|
|
|
163
163
|
- [ ] **Storybook/Demo page**
|
|
164
|
-
-
|
|
165
|
-
-
|
|
164
|
+
- Interactive examples for all use cases
|
|
165
|
+
- States: loading, error, disabled, readonly
|
|
166
166
|
|
|
167
167
|
---
|
|
168
168
|
|
|
169
|
-
##
|
|
169
|
+
## Prioritized Next Steps
|
|
170
170
|
|
|
171
|
-
1. **Live regions** (accessibility -
|
|
172
|
-
2. **Form integration** (
|
|
173
|
-
3.
|
|
174
|
-
4. **
|
|
175
|
-
5. **Async data support** (
|
|
171
|
+
1. **Live regions** (accessibility - high impact)
|
|
172
|
+
2. **Form integration** (usability - common cases)
|
|
173
|
+
3. **Customizable `filterFn`** (scalability)
|
|
174
|
+
4. **Consolidate effects** (performance/best practices)
|
|
175
|
+
5. **Async data support** (scalability)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ComboBox Button
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### ComboBox.Button
|
|
6
|
+
|
|
7
|
+
Name: `ComboBox.Button`
|
|
8
|
+
Description: Optional trigger button that toggles the combobox popover without stealing focus from the input.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| -------------- | ---------------------- | ----------- | --------------------------------------------------------------- |
|
|
12
|
+
| `class` | `string` | `undefined` | CSS class names for the button element. |
|
|
13
|
+
| `children` | `Snippet` | `undefined` | Custom trigger content. If omitted, a chevron icon is rendered. |
|
|
14
|
+
| `tabindex` | `number` | `-1` | Tab index applied to the button. |
|
|
15
|
+
| `...restProps` | `HTMLButtonAttributes` | `-` | Additional native button attributes. |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ComboBox Input
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### ComboBox.Input
|
|
6
|
+
|
|
7
|
+
Name: `ComboBox.Input`
|
|
8
|
+
Description: Text input with `role="combobox"` that syncs typed value, active descendant, and keyboard handling with `ComboBox.Root`.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| ------------------ | --------------------- | ----------- | ----------------------------------------------- |
|
|
12
|
+
| `aria-label` | `string` | `undefined` | Accessible label applied directly to the input. |
|
|
13
|
+
| `aria-labelledby` | `string` | `undefined` | Element id that labels the input. |
|
|
14
|
+
| `aria-describedby` | `string` | `undefined` | Element id that describes the input behavior. |
|
|
15
|
+
| `class` | `string` | `undefined` | CSS class names for the input element. |
|
|
16
|
+
| `...restProps` | `HTMLInputAttributes` | `-` | Additional native input attributes. |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ComboBox Item
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### ComboBox.Item
|
|
6
|
+
|
|
7
|
+
Name: `ComboBox.Item`
|
|
8
|
+
Description: Selectable option item with combobox-specific filtering, virtual focus, and registration logic.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| -------------- | -------------------------------- | -------------- | -------------------------------------------------------------- |
|
|
12
|
+
| `id` | `string \| number` | `required` | Unique item id used for selection and ARIA relationships. |
|
|
13
|
+
| `textValue` | `string` | `content text` | Text used for filtering and fallback label resolution. |
|
|
14
|
+
| `disabled` | `boolean` | `false` | Marks the item as disabled and non-selectable. |
|
|
15
|
+
| `class` | `string` | `undefined` | CSS class names for the item. |
|
|
16
|
+
| `children` | `Snippet` | `undefined` | Rendered item content. |
|
|
17
|
+
| `...restProps` | `HTMLAttributes<HTMLDivElement>` | `-` | Additional attributes passed to the underlying option element. |
|
|
18
|
+
|
|
19
|
+
### Item context utility
|
|
20
|
+
|
|
21
|
+
Name: `COMBOBOX_ITEM_CONTEXT_KEY` / `ComboBoxItemContext`
|
|
22
|
+
Description: Internal context consumed by `ComboBox.ItemIndicator` to read the parent item id.
|
|
23
|
+
|
|
24
|
+
| Prop | Type | Default | Description |
|
|
25
|
+
| --------------------------- | ------------------ | ----------------------------- | ----------------------------------------- |
|
|
26
|
+
| `COMBOBOX_ITEM_CONTEXT_KEY` | `symbol` | `Symbol.for('combobox-item')` | Context key shared by item and indicator. |
|
|
27
|
+
| `id` | `string \| number` | `required` | Current item id exposed through context. |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ComboBox Item Indicator
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### ComboBox.ItemIndicator
|
|
6
|
+
|
|
7
|
+
Name: `ComboBox.ItemIndicator`
|
|
8
|
+
Description: Visual selection indicator rendered inside `ComboBox.Item`.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| -------------- | --------------------------------- | ------------ | ---------------------------------------------------- |
|
|
12
|
+
| `children` | `Snippet` | `check icon` | Custom indicator content. |
|
|
13
|
+
| `forceMount` | `boolean` | `false` | Forces rendering even when the item is not selected. |
|
|
14
|
+
| `class` | `string` | `undefined` | CSS class names for the indicator wrapper. |
|
|
15
|
+
| `...restProps` | `HTMLAttributes<HTMLSpanElement>` | `-` | Additional attributes passed to the wrapper span. |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# ComboBox List
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### ComboBox.List
|
|
6
|
+
|
|
7
|
+
Name: `ComboBox.List`
|
|
8
|
+
Description: Listbox bridge for combobox options. It delegates selection state and mode to `ComboBox.Root`.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| ----------------- | --------------------------------------------------- | ----------- | ------------------------------------------------------------------------------- |
|
|
12
|
+
| `aria-label` | `string` | `'Options'` | Accessible label for the internal listbox. |
|
|
13
|
+
| `items` | `Iterable<T>` | `ctx.items` | Dynamic data source. If omitted, uses items from root context. |
|
|
14
|
+
| `children` | `Snippet<[T]> \| Snippet` | `undefined` | Dynamic item renderer or static list content. |
|
|
15
|
+
| `...listBoxProps` | `Omit<ComponentProps<ListBoxRoot>, internal props>` | `-` | Additional listbox props such as `class`, `emptyPlaceholder`, or `disabledIds`. |
|
|
16
|
+
|
|
17
|
+
### Internally controlled props
|
|
18
|
+
|
|
19
|
+
Name: Controlled by `ComboBox.Root`
|
|
20
|
+
Description: The following values are set by `ComboBox.List` and should not be overridden directly.
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
| --------------- | ------------------------ | -------------------------------- | ----------------------------------------------------- |
|
|
24
|
+
| `selectionMode` | `'single' \| 'multiple'` | `ctx.selectionMode` | Selection mode inherited from root. |
|
|
25
|
+
| `value` | `Set<string \| number>` | `ctx.selectedValue` | Current combobox selection state. |
|
|
26
|
+
| `onChange` | `(selection) => void` | `internal handler` | Delegates selected item handling to combobox context. |
|
|
27
|
+
| `id` | `string` | `combobox-listbox-${instanceId}` | Auto-generated listbox id for ARIA wiring. |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# ComboBox Popover
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### ComboBox.Popover
|
|
6
|
+
|
|
7
|
+
Name: `ComboBox.Popover`
|
|
8
|
+
Description: Floating container for combobox options. Internally composes `Popover.Root` and `Popover.Content` in non-modal mode.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| ---------- | --------- | ----------- | ------------------------------------------- |
|
|
12
|
+
| `class` | `string` | `''` | CSS class names for the floating panel. |
|
|
13
|
+
| `children` | `Snippet` | `undefined` | Popover content, typically `ComboBox.List`. |
|