foldkit 0.20.0 → 0.22.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.
- package/README.md +2 -1
- package/dist/html/index.d.ts +90 -19
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +36 -6
- package/dist/html/public.d.ts +1 -1
- package/dist/html/public.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +16 -16
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +8 -8
- package/dist/task/dom.d.ts +13 -0
- package/dist/task/dom.d.ts.map +1 -1
- package/dist/task/dom.js +38 -1
- package/dist/task/elementMovement.d.ts +21 -0
- package/dist/task/elementMovement.d.ts.map +1 -0
- package/dist/task/elementMovement.js +48 -0
- package/dist/task/index.d.ts +3 -1
- package/dist/task/index.d.ts.map +1 -1
- package/dist/task/index.js +2 -1
- package/dist/task/scrollLock.d.ts +3 -0
- package/dist/task/scrollLock.d.ts.map +1 -1
- package/dist/task/scrollLock.js +36 -0
- package/dist/ui/dialog/index.d.ts.map +1 -1
- package/dist/ui/dialog/index.js +4 -10
- package/dist/ui/disclosure/index.d.ts.map +1 -1
- package/dist/ui/disclosure/index.js +4 -7
- package/dist/ui/menu/anchor.d.ts +18 -0
- package/dist/ui/menu/anchor.d.ts.map +1 -0
- package/dist/ui/menu/anchor.js +72 -0
- package/dist/ui/menu/index.d.ts +11 -5
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +45 -38
- package/dist/ui/menu/public.d.ts +1 -0
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/ui/menu/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { m } from '../../message';
|
|
|
5
5
|
import { evo } from '../../struct';
|
|
6
6
|
import * as Task from '../../task';
|
|
7
7
|
import { findFirstEnabledIndex, keyToIndex, wrapIndex } from '../keyboard';
|
|
8
|
+
import { anchorHooks } from './anchor';
|
|
8
9
|
// MODEL
|
|
9
10
|
/** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
|
|
10
11
|
export const ActivationTrigger = S.Literal('Pointer', 'Keyboard');
|
|
@@ -37,7 +38,7 @@ export const Opened = m('Opened', {
|
|
|
37
38
|
});
|
|
38
39
|
/** Sent when the menu closes via Escape key or backdrop click. */
|
|
39
40
|
export const Closed = m('Closed');
|
|
40
|
-
/** Sent when focus leaves the menu items container via Tab key. */
|
|
41
|
+
/** Sent when focus leaves the menu items container via Tab key or blur. */
|
|
41
42
|
export const ClosedByTab = m('ClosedByTab');
|
|
42
43
|
/** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
|
|
43
44
|
export const ActivatedItem = m('ActivatedItem', {
|
|
@@ -71,6 +72,8 @@ export const NoOp = m('NoOp');
|
|
|
71
72
|
export const AdvancedTransitionFrame = m('AdvancedTransitionFrame');
|
|
72
73
|
/** Sent internally when all CSS transitions on the menu items container have completed. */
|
|
73
74
|
export const EndedTransition = m('EndedTransition');
|
|
75
|
+
/** Sent internally when the menu button moves in the viewport during a leave transition, cancelling the animation. */
|
|
76
|
+
export const DetectedButtonMovement = m('DetectedButtonMovement');
|
|
74
77
|
/** Sent when the user presses a pointer device on the menu button. Records pointer type and toggles for mouse. */
|
|
75
78
|
export const PressedPointerOnButton = m('PressedPointerOnButton', {
|
|
76
79
|
pointerType: S.String,
|
|
@@ -86,7 +89,7 @@ export const ReleasedPointerOnItems = m('ReleasedPointerOnItems', {
|
|
|
86
89
|
timeStamp: S.Number,
|
|
87
90
|
});
|
|
88
91
|
/** Union of all messages the menu component can produce. */
|
|
89
|
-
export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, PressedPointerOnButton, ReleasedPointerOnItems);
|
|
92
|
+
export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, PressedPointerOnButton, ReleasedPointerOnItems);
|
|
90
93
|
// INIT
|
|
91
94
|
const SEARCH_DEBOUNCE_MILLISECONDS = 350;
|
|
92
95
|
const POINTER_HOLD_THRESHOLD_MILLISECONDS = 200;
|
|
@@ -96,7 +99,7 @@ export const init = (config) => ({
|
|
|
96
99
|
id: config.id,
|
|
97
100
|
isOpen: false,
|
|
98
101
|
isAnimated: config.isAnimated ?? false,
|
|
99
|
-
isModal: config.isModal ??
|
|
102
|
+
isModal: config.isModal ?? false,
|
|
100
103
|
transitionState: 'Idle',
|
|
101
104
|
maybeActiveItemIndex: Option.none(),
|
|
102
105
|
activationTrigger: 'Keyboard',
|
|
@@ -121,17 +124,18 @@ const closedModel = (model) => evo(model, {
|
|
|
121
124
|
const buttonSelector = (id) => `#${id}-button`;
|
|
122
125
|
const itemsSelector = (id) => `#${id}-items`;
|
|
123
126
|
const itemSelector = (id, index) => `#${id}-item-${index}`;
|
|
127
|
+
const withUpdateReturn = M.withReturnType();
|
|
124
128
|
/** Processes a menu message and returns the next model and commands. */
|
|
125
129
|
export const update = (model, message) => {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
+
const maybeNextFrame = OptionExt.when(model.isAnimated, Task.nextFrame.pipe(Effect.as(AdvancedTransitionFrame())));
|
|
131
|
+
const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(NoOp())));
|
|
132
|
+
const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(NoOp())));
|
|
133
|
+
const maybeInertOthers = OptionExt.when(model.isModal, Task.inertOthers(model.id, [
|
|
130
134
|
buttonSelector(model.id),
|
|
131
135
|
itemsSelector(model.id),
|
|
132
136
|
]).pipe(Effect.as(NoOp())));
|
|
133
|
-
const
|
|
134
|
-
return M.value(message).pipe(
|
|
137
|
+
const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(NoOp())));
|
|
138
|
+
return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
|
|
135
139
|
Opened: ({ maybeActiveItemIndex }) => {
|
|
136
140
|
const nextModel = evo(model, {
|
|
137
141
|
isOpen: () => true,
|
|
@@ -147,28 +151,20 @@ export const update = (model, message) => {
|
|
|
147
151
|
});
|
|
148
152
|
return [
|
|
149
153
|
nextModel,
|
|
150
|
-
pipe(Array.getSomes([
|
|
151
|
-
maybeNextFrameCommand,
|
|
152
|
-
maybeLockScrollCommand,
|
|
153
|
-
maybeInertOthersCommand,
|
|
154
|
-
]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
154
|
+
pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
155
155
|
];
|
|
156
156
|
},
|
|
157
157
|
Closed: () => [
|
|
158
158
|
closedModel(model),
|
|
159
159
|
pipe(Array.getSomes([
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
maybeNextFrame,
|
|
161
|
+
maybeUnlockScroll,
|
|
162
|
+
maybeRestoreInert,
|
|
163
163
|
]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
164
164
|
],
|
|
165
165
|
ClosedByTab: () => [
|
|
166
166
|
closedModel(model),
|
|
167
|
-
Array.getSomes([
|
|
168
|
-
maybeNextFrameCommand,
|
|
169
|
-
maybeUnlockScrollCommand,
|
|
170
|
-
maybeRestoreInertCommand,
|
|
171
|
-
]),
|
|
167
|
+
Array.getSomes([maybeNextFrame, maybeUnlockScroll, maybeRestoreInert]),
|
|
172
168
|
],
|
|
173
169
|
ActivatedItem: ({ index, activationTrigger }) => [
|
|
174
170
|
evo(model, {
|
|
@@ -201,9 +197,9 @@ export const update = (model, message) => {
|
|
|
201
197
|
SelectedItem: () => [
|
|
202
198
|
closedModel(model),
|
|
203
199
|
pipe(Array.getSomes([
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
maybeNextFrame,
|
|
201
|
+
maybeUnlockScroll,
|
|
202
|
+
maybeRestoreInert,
|
|
207
203
|
]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
208
204
|
],
|
|
209
205
|
RequestedItemClick: ({ index }) => [
|
|
@@ -232,7 +228,7 @@ export const update = (model, message) => {
|
|
|
232
228
|
}
|
|
233
229
|
return [evo(model, { searchQuery: () => '' }), []];
|
|
234
230
|
},
|
|
235
|
-
AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(
|
|
231
|
+
AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterStart', () => [
|
|
236
232
|
evo(model, { transitionState: () => 'EnterAnimating' }),
|
|
237
233
|
[
|
|
238
234
|
Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition())),
|
|
@@ -240,10 +236,14 @@ export const update = (model, message) => {
|
|
|
240
236
|
]), M.when('LeaveStart', () => [
|
|
241
237
|
evo(model, { transitionState: () => 'LeaveAnimating' }),
|
|
242
238
|
[
|
|
243
|
-
Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition())),
|
|
239
|
+
Effect.raceFirst(Task.detectElementMovement(buttonSelector(model.id)).pipe(Effect.as(DetectedButtonMovement())), Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition()))),
|
|
244
240
|
],
|
|
245
241
|
]), M.orElse(() => [model, []])),
|
|
246
|
-
EndedTransition: () => M.value(model.transitionState).pipe(
|
|
242
|
+
EndedTransition: () => M.value(model.transitionState).pipe(withUpdateReturn, M.whenOr('EnterAnimating', 'LeaveAnimating', () => [
|
|
243
|
+
evo(model, { transitionState: () => 'Idle' }),
|
|
244
|
+
[],
|
|
245
|
+
]), M.orElse(() => [model, []])),
|
|
246
|
+
DetectedButtonMovement: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('LeaveAnimating', () => [
|
|
247
247
|
evo(model, { transitionState: () => 'Idle' }),
|
|
248
248
|
[],
|
|
249
249
|
]), M.orElse(() => [model, []])),
|
|
@@ -258,9 +258,9 @@ export const update = (model, message) => {
|
|
|
258
258
|
return [
|
|
259
259
|
closedModel(withPointerType),
|
|
260
260
|
pipe(Array.getSomes([
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
maybeNextFrame,
|
|
262
|
+
maybeUnlockScroll,
|
|
263
|
+
maybeRestoreInert,
|
|
264
264
|
]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
265
265
|
];
|
|
266
266
|
}
|
|
@@ -276,11 +276,7 @@ export const update = (model, message) => {
|
|
|
276
276
|
});
|
|
277
277
|
return [
|
|
278
278
|
nextModel,
|
|
279
|
-
pipe(Array.getSomes([
|
|
280
|
-
maybeNextFrameCommand,
|
|
281
|
-
maybeLockScrollCommand,
|
|
282
|
-
maybeInertOthersCommand,
|
|
283
|
-
]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
279
|
+
pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
284
280
|
];
|
|
285
281
|
},
|
|
286
282
|
ReleasedPointerOnItems: ({ screenX, screenY, timeStamp }) => {
|
|
@@ -333,8 +329,8 @@ export const resolveTypeaheadMatch = (items, query, maybeActiveItemIndex, isDisa
|
|
|
333
329
|
};
|
|
334
330
|
/** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
|
|
335
331
|
export const view = (config) => {
|
|
336
|
-
const { div, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, Class, DataAttribute, Id, OnBlur, OnClick, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, OnPointerUp, Role, Tabindex, Type, keyed, } = html();
|
|
337
|
-
const { model: { id, isOpen, transitionState, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, itemToSearchText = (item) => item, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, } = config;
|
|
332
|
+
const { div, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, Class, DataAttribute, Id, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, OnPointerUp, Role, Style, Tabindex, Type, keyed, } = html();
|
|
333
|
+
const { model: { id, isOpen, transitionState, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, itemToSearchText = (item) => item, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, anchor, } = config;
|
|
338
334
|
const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
|
|
339
335
|
const isVisible = isOpen || isLeaving;
|
|
340
336
|
const transitionAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
|
|
@@ -415,6 +411,16 @@ export const view = (config) => {
|
|
|
415
411
|
onNone: () => [],
|
|
416
412
|
onSome: index => [AriaActiveDescendant(itemId(id, index))],
|
|
417
413
|
});
|
|
414
|
+
const hooks = anchor
|
|
415
|
+
? anchorHooks({ buttonId: `${id}-button`, anchor })
|
|
416
|
+
: undefined;
|
|
417
|
+
const anchorAttributes = hooks
|
|
418
|
+
? [
|
|
419
|
+
Style({ position: 'absolute', margin: '0', visibility: 'hidden' }),
|
|
420
|
+
OnInsert(hooks.onInsert),
|
|
421
|
+
OnDestroy(hooks.onDestroy),
|
|
422
|
+
]
|
|
423
|
+
: [];
|
|
418
424
|
const itemsContainerAttributes = [
|
|
419
425
|
Id(`${id}-items`),
|
|
420
426
|
Role('menu'),
|
|
@@ -422,6 +428,7 @@ export const view = (config) => {
|
|
|
422
428
|
...maybeActiveDescendant,
|
|
423
429
|
Tabindex(0),
|
|
424
430
|
Class(itemsClassName),
|
|
431
|
+
...anchorAttributes,
|
|
425
432
|
...transitionAttributes,
|
|
426
433
|
...(isLeaving
|
|
427
434
|
? []
|
package/dist/ui/menu/public.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { init, update, view, Model, Message, TransitionState } from './index';
|
|
2
2
|
export type { ActivationTrigger, Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, InitConfig, ViewConfig, ItemConfig, GroupHeading, } from './index';
|
|
3
|
+
export type { AnchorConfig } from './anchor';
|
|
3
4
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,aAAa,EACb,IAAI,EACJ,uBAAuB,EACvB,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA"}
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,aAAa,EACb,IAAI,EACJ,uBAAuB,EACvB,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Elm-inspired UI framework powered by Effect",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"directory": "packages/foldkit"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
|
+
"@floating-ui/dom": "^1.7.5",
|
|
109
110
|
"snabbdom": "^3.6.3"
|
|
110
111
|
},
|
|
111
112
|
"publishConfig": {
|