ajo-ui 0.1.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.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
@@ -0,0 +1,261 @@
1
+ import { remember } from 'ajo-cloves'
2
+ import { fromISO, type SegmentsKind } from './segments'
3
+
4
+ /** Half-open wall-time interval used by availability expressions. */
5
+ export type TimeWindow = {
6
+ /** Inclusive wall-time start, `HH:MM[:SS]`. */
7
+ from?: string
8
+ /** Exclusive wall-time end, `HH:MM[:SS]`. */
9
+ to?: string
10
+ }
11
+
12
+ type DateMatcher = {
13
+ after?: Date
14
+ before?: Date
15
+ dayOfWeek?: number[]
16
+ from?: Date
17
+ to?: Date
18
+ }
19
+
20
+ /** Day-granular matcher grammar shared by Calendar and date fields. */
21
+ export type CalendarMatcher =
22
+ | ((date: Date) => boolean)
23
+ | DateMatcher
24
+ | Date
25
+ | Date[]
26
+
27
+ /** Calendar matcher with an optional half-open wall-time dimension. */
28
+ export type AvailabilityMatcher =
29
+ | CalendarMatcher
30
+ | (DateMatcher & { time: TimeWindow })
31
+
32
+ type DateParts = {
33
+ day: number
34
+ hour: number
35
+ minute: number
36
+ month: number
37
+ second: number
38
+ year: number
39
+ }
40
+
41
+ /** Stable compiled availability view. */
42
+ export type Availability = {
43
+ day(date: Date): boolean
44
+ at(date: Date): boolean
45
+ value(kind: SegmentsKind, value: string): boolean
46
+ crosses(from: Date, to: Date): boolean
47
+ }
48
+
49
+ export type AvailabilityCompileOptions = {
50
+ /** Calendar time zone used for matcher and candidate date parts. */
51
+ timeZone?: string
52
+ }
53
+
54
+ const clock = (value: string | undefined, fallback: number) => {
55
+ if (value == null) return fallback
56
+ const match = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value)
57
+ if (!match) return Number.NaN
58
+ const hour = Number(match[1])
59
+ const minute = Number(match[2])
60
+ const second = Number(match[3] ?? 0)
61
+ if (hour > 23 || minute > 59 || second > 59) return Number.NaN
62
+ return hour * 3600 + minute * 60 + second
63
+ }
64
+
65
+ const timePredicate = (window: TimeWindow) => {
66
+ const from = clock(window.from, 0)
67
+ const to = clock(window.to, 24 * 3600)
68
+ const valid = Number.isFinite(from) && Number.isFinite(to) && from < to
69
+ return (parts: DateParts) => {
70
+ const value = parts.hour * 3600 + parts.minute * 60 + parts.second
71
+ return valid && from <= value && value < to
72
+ }
73
+ }
74
+
75
+ const partFormatters = new Map<string, Intl.DateTimeFormat>()
76
+
77
+ const partFormatter = (timeZone: string) => {
78
+ let formatter = partFormatters.get(timeZone)
79
+ if (!formatter) {
80
+ formatter = new Intl.DateTimeFormat('en-CA', {
81
+ day: '2-digit',
82
+ hour: '2-digit',
83
+ hourCycle: 'h23',
84
+ minute: '2-digit',
85
+ month: '2-digit',
86
+ second: '2-digit',
87
+ timeZone,
88
+ year: 'numeric',
89
+ })
90
+ remember(partFormatters, timeZone, formatter)
91
+ }
92
+ return formatter
93
+ }
94
+
95
+ const partsOf = (date: Date, timeZone?: string): DateParts => {
96
+ if (!Number.isFinite(date.getTime())) return {
97
+ day: Number.NaN,
98
+ hour: Number.NaN,
99
+ minute: Number.NaN,
100
+ month: Number.NaN,
101
+ second: Number.NaN,
102
+ year: Number.NaN,
103
+ }
104
+ if (!timeZone) return {
105
+ day: date.getDate(),
106
+ hour: date.getHours(),
107
+ minute: date.getMinutes(),
108
+ month: date.getMonth() + 1,
109
+ second: date.getSeconds(),
110
+ year: date.getFullYear(),
111
+ }
112
+ const parts = partFormatter(timeZone).formatToParts(date)
113
+ const value = (type: Intl.DateTimeFormatPartTypes) => Number(parts.find(part => part.type === type)?.value)
114
+ return {
115
+ day: value('day'),
116
+ hour: value('hour'),
117
+ minute: value('minute'),
118
+ month: value('month'),
119
+ second: value('second'),
120
+ year: value('year'),
121
+ }
122
+ }
123
+
124
+ const dayValue = (parts: Pick<DateParts, 'day' | 'month' | 'year'>) =>
125
+ parts.year * 10000 + parts.month * 100 + parts.day
126
+
127
+ const exactUtcDate = (parts: Pick<DateParts, 'day' | 'month' | 'year'> & Partial<Pick<DateParts, 'hour' | 'minute' | 'second'>>) => {
128
+ const date = new Date(0)
129
+ date.setUTCFullYear(parts.year, parts.month - 1, parts.day)
130
+ date.setUTCHours(parts.hour ?? 0, parts.minute ?? 0, parts.second ?? 0, 0)
131
+ return date
132
+ }
133
+
134
+ const exactLocalDate = (parts: DateParts) => {
135
+ const date = new Date(0)
136
+ date.setFullYear(parts.year, parts.month - 1, parts.day)
137
+ date.setHours(parts.hour, parts.minute, parts.second, 0)
138
+ return date
139
+ }
140
+
141
+ const weekday = (parts: Pick<DateParts, 'day' | 'month' | 'year'>) =>
142
+ exactUtcDate(parts).getUTCDay()
143
+
144
+ /** Constructs a stable instant for one wall-clock value in a calendar time zone. */
145
+ export const calendarDate = (parts: DateParts, timeZone?: string) => {
146
+ if (!timeZone) return exactLocalDate(parts)
147
+ const wanted = exactUtcDate(parts).getTime()
148
+ let instant = wanted
149
+ for (let pass = 0; pass < 2; pass++) {
150
+ const observed = partsOf(new Date(instant), timeZone)
151
+ const actual = exactUtcDate(observed).getTime()
152
+ instant += wanted - actual
153
+ }
154
+ return new Date(instant)
155
+ }
156
+
157
+ const addDay = (parts: DateParts): DateParts => {
158
+ const next = exactUtcDate({ ...parts, day: parts.day + 1, hour: 12 })
159
+ return {
160
+ day: next.getUTCDate(),
161
+ hour: 12,
162
+ minute: 0,
163
+ month: next.getUTCMonth() + 1,
164
+ second: 0,
165
+ year: next.getUTCFullYear(),
166
+ }
167
+ }
168
+
169
+ /** Compiles date/time expressions into stable day- and instant-granular predicates. */
170
+ export const compile = (
171
+ matcher: AvailabilityMatcher | AvailabilityMatcher[] | undefined,
172
+ options: AvailabilityCompileOptions = {},
173
+ ): Availability | undefined => {
174
+ if (matcher == null) return undefined
175
+ const matchers = (Array.isArray(matcher) ? matcher : [matcher]) as AvailabilityMatcher[]
176
+ if (!matchers.length) return undefined
177
+ const timeZone = options.timeZone
178
+ const parts = (date: Date) => partsOf(date, timeZone)
179
+ type Predicate = {
180
+ date: (value: Date, current: () => DateParts) => boolean
181
+ time?: (current: DateParts) => boolean
182
+ }
183
+ const compileMatcher = (item: AvailabilityMatcher): Predicate => {
184
+ if (item instanceof Date) {
185
+ const expected = dayValue(parts(item))
186
+ return { date: (_value, current) => dayValue(current()) === expected }
187
+ }
188
+ if (Array.isArray(item)) {
189
+ const expected = new Set(item
190
+ .map(entry => dayValue(parts(entry)))
191
+ .filter(Number.isFinite))
192
+ return { date: (_value, current) => expected.has(dayValue(current())) }
193
+ }
194
+ if (typeof item === 'function') return { date: item }
195
+
196
+ const {
197
+ after,
198
+ before,
199
+ dayOfWeek,
200
+ from,
201
+ to,
202
+ } = item
203
+ const afterDay = after == null ? undefined : dayValue(parts(after))
204
+ const beforeDay = before == null ? undefined : dayValue(parts(before))
205
+ const fromDay = from == null ? undefined : dayValue(parts(from))
206
+ const toDay = to == null ? undefined : dayValue(parts(to))
207
+ const weekdays = dayOfWeek == null ? undefined : new Set(dayOfWeek)
208
+ const hasDate = after != null || before != null || dayOfWeek != null || from != null || to != null
209
+ const time = 'time' in item ? timePredicate(item.time) : undefined
210
+
211
+ return {
212
+ date: (_value, read) => {
213
+ if (!hasDate) return time != null
214
+ const current = read()
215
+ const currentDay = dayValue(current)
216
+ if (weekdays != null && !weekdays.has(weekday(current))) return false
217
+ if (beforeDay != null && !(currentDay < beforeDay)) return false
218
+ if (afterDay != null && !(currentDay > afterDay)) return false
219
+ if (fromDay != null && toDay != null) return fromDay <= currentDay && currentDay <= toDay
220
+ if (fromDay != null) return currentDay === fromDay
221
+ if (toDay != null) return currentDay === toDay
222
+ return true
223
+ },
224
+ time,
225
+ }
226
+ }
227
+ const predicates = matchers.map(compileMatcher)
228
+ const day = (date: Date) => {
229
+ let current: DateParts | undefined
230
+ const read = () => current ??= parts(date)
231
+ return predicates.some(predicate => predicate.time == null && predicate.date(date, read))
232
+ }
233
+ const at = (date: Date) => {
234
+ let current: DateParts | undefined
235
+ const read = () => current ??= parts(date)
236
+ return predicates.some(predicate => predicate.date(date, read) && (predicate.time?.(read()) ?? true))
237
+ }
238
+
239
+ return {
240
+ day,
241
+ at,
242
+ value(kind: SegmentsKind, value: string) {
243
+ const units = fromISO(kind, value)
244
+ if (!units) return false
245
+ const now = parts(new Date())
246
+ const date = kind === 'time'
247
+ ? calendarDate({ ...now, hour: units.hour ?? 0, minute: units.minute ?? 0, second: units.second ?? 0 }, timeZone)
248
+ : calendarDate({ day: units.day!, hour: units.hour ?? 12, minute: units.minute ?? 0, month: units.month!, second: units.second ?? 0, year: units.year! }, timeZone)
249
+ return kind === 'date' ? day(date) : at(date)
250
+ },
251
+ crosses(from: Date, to: Date) {
252
+ const end = dayValue(parts(to))
253
+ let current = parts(from)
254
+ if (dayValue(current) >= end) return false
255
+ for (current = addDay(current); dayValue(current) < end; current = addDay(current)) {
256
+ if (day(calendarDate(current, timeZone))) return true
257
+ }
258
+ return false
259
+ },
260
+ }
261
+ }
package/src/avatar.tsx ADDED
@@ -0,0 +1,99 @@
1
+ import type { IntrinsicElements, Stateless, WithChildren } from 'ajo'
2
+ import { callHandler } from 'ajo-cloves'
3
+
4
+ /** Arguments for the Avatar root. */
5
+ export type AvatarArgs = WithChildren<IntrinsicElements['div'] & {
6
+ /** Size marker mirrored as `data-size`. */
7
+ size?: string
8
+ }>
9
+
10
+ /** Arguments for the native image inside an Avatar. */
11
+ export type AvatarImageArgs = IntrinsicElements['img']
12
+ /** Arguments for fallback content shown without a usable image. */
13
+ export type AvatarFallbackArgs = WithChildren<IntrinsicElements['span']>
14
+ /** Arguments for the status badge attached to an Avatar. */
15
+ export type AvatarBadgeArgs = WithChildren<IntrinsicElements['span']>
16
+ /** Arguments for a visual group of overlapping avatars. */
17
+ export type AvatarGroupArgs = WithChildren<IntrinsicElements['div']>
18
+ /** Arguments for the overflow count in an Avatar group. */
19
+ export type AvatarGroupCountArgs = WithChildren<IntrinsicElements['div']>
20
+
21
+ /** Unstyled avatar root that exposes image, fallback, and badge slots. */
22
+ const Avatar: Stateless<AvatarArgs> = ({
23
+ children,
24
+ size = 'default',
25
+ ...attrs
26
+ }) => (
27
+ <div
28
+ {...attrs}
29
+ data-size={size}
30
+ data-slot="avatar"
31
+ >
32
+ {children}
33
+ </div>
34
+ )
35
+
36
+ /** Unstyled avatar image that hides itself after load errors. */
37
+ const AvatarImage: Stateless<AvatarImageArgs> = ({
38
+ alt = '',
39
+ decoding = 'async',
40
+ loading = 'lazy',
41
+ 'set:onerror': onerror,
42
+ 'set:onload': onload,
43
+ ...attrs
44
+ }) => (
45
+ <img
46
+ {...attrs}
47
+ alt={alt}
48
+ data-slot="avatar-image"
49
+ decoding={decoding}
50
+ loading={loading}
51
+ set:onerror={(event: Event | string) => {
52
+ if (event instanceof Event) {
53
+ ;(event.currentTarget as HTMLImageElement).hidden = true
54
+ callHandler(onerror, event)
55
+ }
56
+ }}
57
+ set:onload={(event: Event) => {
58
+ ;(event.currentTarget as HTMLImageElement).hidden = false
59
+ callHandler(onload, event)
60
+ }}
61
+ />
62
+ )
63
+
64
+ /** Unstyled fallback content for an Avatar. */
65
+ const AvatarFallback: Stateless<AvatarFallbackArgs> = ({ children, ...attrs }) => (
66
+ <span {...attrs} data-slot="avatar-fallback">
67
+ {children}
68
+ </span>
69
+ )
70
+
71
+ /** Unstyled badge anchored to an Avatar. */
72
+ const AvatarBadge: Stateless<AvatarBadgeArgs> = ({ children, ...attrs }) => (
73
+ <span {...attrs} data-slot="avatar-badge">
74
+ {children}
75
+ </span>
76
+ )
77
+
78
+ /** Unstyled container for a group of avatars. */
79
+ const AvatarGroup: Stateless<AvatarGroupArgs> = ({ children, ...attrs }) => (
80
+ <div {...attrs} data-slot="avatar-group">
81
+ {children}
82
+ </div>
83
+ )
84
+
85
+ /** Unstyled count for avatars omitted from a visible group. */
86
+ const AvatarGroupCount: Stateless<AvatarGroupCountArgs> = ({ children, ...attrs }) => (
87
+ <div {...attrs} data-slot="avatar-group-count">
88
+ {children}
89
+ </div>
90
+ )
91
+
92
+ export {
93
+ Avatar,
94
+ AvatarBadge,
95
+ AvatarFallback,
96
+ AvatarGroup,
97
+ AvatarGroupCount,
98
+ AvatarImage,
99
+ }
package/src/bar.ts ADDED
@@ -0,0 +1,156 @@
1
+ import type { Host } from 'ajo-cloves'
2
+ import { controlled, dom, roving, typeahead } from 'ajo-cloves'
3
+
4
+ export type BarView = {
5
+ /** Open trigger value; '' = every surface closed. */
6
+ readonly value: string
7
+ /** Stored roving tab-stop value; '' = defer to DOM order. */
8
+ readonly focused: string
9
+ /** Reads controlled/uncontrolled value for this render; call once per render pass. */
10
+ sync(value: string | undefined): string
11
+ /** Opens a value, or closes everything with ''. */
12
+ setValue(next: string, event?: Event): void
13
+ close(event?: Event): void
14
+ /** Follow policy: while the bar is open, moves the open value to the entered trigger; inert while closed. */
15
+ follow(next: string, event?: Event): void
16
+ /** Focus policy: adopts the roving tab stop, and follows while the bar is open. */
17
+ focus(next: string, event?: Event): void
18
+ /** Seeds the tab stop without invalidating (registration repair). */
19
+ adopt(next: string): void
20
+ /** Single tab stop: true when the value holds it, resolved against the live row. */
21
+ isTabbable(value: string): boolean
22
+ /** Trigger-row keydown skeleton: roving arrows, typeahead, and Escape gated on open. */
23
+ handle(event: KeyboardEvent): boolean
24
+ }
25
+
26
+ export type BarOptions = {
27
+ /** Live enabled trigger row, in DOM order. */
28
+ triggers: () => HTMLElement[]
29
+ /** Bar value of one trigger. Default: dataset.value. */
30
+ value?: (trigger: HTMLElement) => string
31
+ /** Value before the first sync; '' = closed. */
32
+ initialValue?: string
33
+ /** Blocks opening while true. */
34
+ disabled?: () => boolean
35
+ /** Wrap arrow navigation at the ends. Default: true. */
36
+ loop?: () => boolean
37
+ /** Called whenever the open value changes. */
38
+ onValueChange?: (value: string, event?: Event) => void
39
+ }
40
+
41
+ /**
42
+ * The shared root machine for horizontal trigger bars (menubar,
43
+ * navigation-menu): one controlled open value ('' = closed), roving over a
44
+ * live trigger row with typeahead, and the open-follows-focus/hover policy.
45
+ *
46
+ * Movement (arrows, typeahead) only focuses the target trigger; the follow
47
+ * policy runs exactly once through the trigger's own focus handler, so
48
+ * controlled consumers see a single onValueChange per move.
49
+ */
50
+ export const bar = (host: Host, opts: BarOptions): BarView => {
51
+ const read = opts.value ?? ((trigger: HTMLElement) => trigger.dataset.value ?? '')
52
+ const state = controlled<string>(host, {
53
+ fallback: opts.initialValue ?? '',
54
+ onChange: opts.onValueChange,
55
+ })
56
+ let focused = ''
57
+
58
+ // The tab stop resolves against the live row: a stale, removed, or
59
+ // disabled entry falls back to the first trigger in DOM order.
60
+ const stop = () => {
61
+ if (!dom(host)) return ''
62
+ const row = opts.triggers()
63
+ if (!row.length) return ''
64
+ return focused && row.some(trigger => read(trigger) === focused) ? focused : read(row[0])
65
+ }
66
+
67
+ const setValue = (next: string, event?: Event) => {
68
+ if (next && opts.disabled?.()) return
69
+ if (next === state.value) return
70
+ if (next) focused = next
71
+ state.set(next, event)
72
+ }
73
+
74
+ let following = false
75
+
76
+ const follow = (next: string, event?: Event) => {
77
+ if (following) return
78
+ if (!state.value || !next || next === state.value) return
79
+
80
+ following = true
81
+ try {
82
+ // When focus sits inside the surface about to close (a DOM
83
+ // descendant of the bar that is not a row trigger), move it onto
84
+ // the followed trigger first: hiding a native popover restores
85
+ // focus to its invoker, which would re-run the follow policy and
86
+ // bounce the open value straight back.
87
+ if (dom(host)) {
88
+ const row = opts.triggers()
89
+ const target = row.find(trigger => read(trigger) === next)
90
+ const active = document.activeElement as HTMLElement | null
91
+ if (target && active && active !== target && (host as unknown as HTMLElement).contains(active) && !row.includes(active)) {
92
+ target.focus()
93
+ }
94
+ }
95
+ setValue(next, event)
96
+ } finally {
97
+ following = false
98
+ }
99
+ }
100
+
101
+ const nav = roving(host, {
102
+ items: opts.triggers,
103
+ orientation: () => 'horizontal',
104
+ loop: () => opts.loop?.() ?? true,
105
+ // Focus only: the focus event runs the follow policy once.
106
+ onMove: target => target.focus(),
107
+ })
108
+
109
+ const ta = typeahead(host, {
110
+ items: opts.triggers,
111
+ onMatch: (target, event) => {
112
+ event.preventDefault()
113
+ target.focus()
114
+ },
115
+ })
116
+
117
+ return {
118
+ get value() {
119
+ return state.value
120
+ },
121
+ get focused() {
122
+ return focused
123
+ },
124
+ sync: value => state.sync(value),
125
+ setValue,
126
+ close: event => setValue('', event),
127
+ follow,
128
+ focus(next, event) {
129
+ if (state.value && next && next !== state.value) {
130
+ follow(next, event)
131
+ } else if (focused !== next) {
132
+ focused = next
133
+ host.next()
134
+ }
135
+ },
136
+ adopt(next) {
137
+ focused = next
138
+ },
139
+ isTabbable(value) {
140
+ const current = stop()
141
+ return !current || current === value
142
+ },
143
+ handle(event) {
144
+ if (nav.handle(event)) return true
145
+
146
+ if (event.key === 'Escape') {
147
+ if (!state.value) return false
148
+ event.preventDefault()
149
+ setValue('', event)
150
+ return true
151
+ }
152
+
153
+ return ta.handle(event)
154
+ },
155
+ }
156
+ }