@torpor/ui 0.3.7 → 1.0.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 +83 -7
- package/dist/Accordion/Accordion.torp +43 -165
- package/dist/Accordion/AccordionContent.torp +6 -6
- package/dist/Accordion/AccordionHeader.torp +7 -11
- package/dist/Accordion/AccordionItem.torp +24 -24
- package/dist/Accordion/AccordionTrigger.torp +17 -8
- package/dist/Accordion/AccordionTypes.ts +6 -6
- package/dist/Accordion/index.d.ts +23 -25
- package/dist/Accordion/index.js +6 -22
- package/dist/Breadcrumb/Breadcrumb.torp +12 -10
- package/dist/Breadcrumb/BreadcrumbItem.torp +6 -6
- package/dist/Breadcrumb/BreadcrumbList.torp +21 -0
- package/dist/Breadcrumb/BreadcrumbSeparator.torp +4 -4
- package/dist/Breadcrumb/index.d.ts +39 -15
- package/dist/Breadcrumb/index.js +5 -14
- package/dist/Calendar/Calendar.torp +163 -156
- package/dist/Calendar/CalendarDay.torp +91 -45
- package/dist/Calendar/CalendarDayHeader.torp +36 -0
- package/dist/Calendar/CalendarGrid.torp +104 -0
- package/dist/Calendar/CalendarGridHeader.torp +43 -0
- package/dist/Calendar/CalendarHeader.torp +48 -0
- package/dist/Calendar/CalendarHeaderTrigger.torp +54 -0
- package/dist/Calendar/CalendarTypes.ts +25 -4
- package/dist/Calendar/buildDays.ts +54 -0
- package/dist/Calendar/index.d.ts +191 -5
- package/dist/Calendar/index.js +8 -10
- package/dist/Carousel/Carousel.torp +180 -0
- package/dist/Carousel/CarouselIndicators.torp +65 -0
- package/dist/Carousel/CarouselNext.torp +43 -0
- package/dist/Carousel/CarouselPrevious.torp +44 -0
- package/dist/Carousel/CarouselSlide.torp +57 -0
- package/dist/Carousel/CarouselTypes.ts +28 -0
- package/dist/Carousel/index.d.ts +147 -0
- package/dist/Carousel/index.js +6 -0
- package/dist/Charts/BarChart.torp +70 -0
- package/dist/Charts/Chart.torp +120 -0
- package/dist/Charts/ChartBars.torp +78 -0
- package/dist/Charts/ChartColumns.torp +66 -0
- package/dist/Charts/ChartGrid.torp +54 -0
- package/dist/Charts/ChartLines.torp +60 -0
- package/dist/Charts/ChartTypes.ts +25 -0
- package/dist/Charts/ChartXAxis.torp +83 -0
- package/dist/Charts/ChartYAxis.torp +105 -0
- package/dist/Charts/ColumnChart.torp +70 -0
- package/dist/{Chart → Charts}/GridLines.torp +7 -4
- package/dist/Charts/LineChart.torp +73 -0
- package/dist/{Chart → Charts}/PieChart.torp +14 -20
- package/dist/{Chart → Charts}/ScatterChart.torp +45 -38
- package/dist/{Chart → Charts}/SparkLine.torp +16 -19
- package/dist/{Chart → Charts}/XAxis.torp +18 -12
- package/dist/{Chart → Charts}/YAxis.torp +15 -9
- package/dist/Charts/index.d.ts +469 -0
- package/dist/Charts/index.js +17 -0
- package/dist/ColorPicker/ColorPalette.torp +154 -0
- package/dist/ColorPicker/ColorPicker.torp +157 -0
- package/dist/ColorPicker/index.d.ts +85 -0
- package/dist/ColorPicker/index.js +3 -0
- package/dist/ComboBox/ComboBox.torp +205 -0
- package/dist/ComboBox/ComboBoxContent.torp +112 -0
- package/dist/ComboBox/ComboBoxInput.torp +145 -0
- package/dist/ComboBox/ComboBoxTrigger.torp +49 -0
- package/dist/ComboBox/ComboBoxTypes.ts +39 -0
- package/dist/ComboBox/index.d.ts +152 -0
- package/dist/ComboBox/index.js +6 -0
- package/dist/CommandPalette/CommandPalette.torp +220 -0
- package/dist/CommandPalette/CommandTypes.ts +14 -0
- package/dist/CommandPalette/index.d.ts +45 -0
- package/dist/CommandPalette/index.js +2 -0
- package/dist/Contextual/Contextual.torp +39 -52
- package/dist/Contextual/ContextualContent.torp +29 -73
- package/dist/Contextual/ContextualHover.torp +112 -0
- package/dist/Contextual/ContextualTrigger.torp +31 -42
- package/dist/Contextual/ContextualTypes.ts +5 -18
- package/dist/Contextual/index.d.ts +44 -27
- package/dist/Contextual/index.js +5 -14
- package/dist/DataGrid/DataGrid.torp +496 -0
- package/dist/DataGrid/DataGridTypes.ts +25 -0
- package/dist/DataGrid/index.d.ts +90 -0
- package/dist/DataGrid/index.js +3 -0
- package/dist/DataGrid/sortRows.ts +32 -0
- package/dist/DatePicker/DatePicker.torp +222 -0
- package/dist/DatePicker/DatePickerHeader.torp +20 -0
- package/dist/DatePicker/index.d.ts +55 -0
- package/dist/DatePicker/index.js +2 -0
- package/dist/DateRangePicker/DateRangePicker.torp +311 -0
- package/dist/DateRangePicker/DateRangePickerDay.torp +137 -0
- package/dist/DateRangePicker/DateRangePickerTypes.ts +21 -0
- package/dist/DateRangePicker/index.d.ts +76 -0
- package/dist/DateRangePicker/index.js +3 -0
- package/dist/Dialog/Dialog.torp +33 -30
- package/dist/Dialog/DialogBody.torp +5 -9
- package/dist/Dialog/DialogButton.torp +14 -11
- package/dist/Dialog/DialogFooter.torp +3 -7
- package/dist/Dialog/DialogHeader.torp +6 -10
- package/dist/Dialog/DialogTypes.ts +2 -2
- package/dist/Dialog/index.d.ts +28 -26
- package/dist/Dialog/index.js +6 -22
- package/dist/Disclosure/Disclosure.torp +18 -18
- package/dist/Disclosure/DisclosureContent.torp +5 -6
- package/dist/Disclosure/DisclosureHeader.torp +7 -11
- package/dist/Disclosure/DisclosureTrigger.torp +4 -5
- package/dist/Disclosure/DisclosureTypes.ts +1 -1
- package/dist/Disclosure/index.d.ts +14 -16
- package/dist/Disclosure/index.js +5 -18
- package/dist/Drawer/Drawer.torp +21 -51
- package/dist/Drawer/DrawerContent.torp +67 -13
- package/dist/Drawer/DrawerOverlay.torp +4 -4
- package/dist/Drawer/DrawerTrigger.torp +16 -4
- package/dist/Drawer/DrawerTypes.ts +4 -1
- package/dist/Drawer/index.d.ts +16 -20
- package/dist/Drawer/index.js +5 -18
- package/dist/FocusGroup/FocusGroup.torp +70 -0
- package/dist/FocusGroup/FocusGroupButton.torp +59 -0
- package/dist/FocusGroup/FocusGroupTypes.ts +14 -0
- package/dist/FocusGroup/index.d.ts +44 -0
- package/dist/FocusGroup/index.js +3 -0
- package/dist/Form/CheckBox.torp +28 -20
- package/dist/Form/Field.torp +17 -17
- package/dist/Form/File.torp +31 -22
- package/dist/Form/FileDrop.torp +187 -0
- package/dist/Form/Form.torp +24 -22
- package/dist/Form/FormTypes.ts +2 -0
- package/dist/Form/FormValues.torp +37 -0
- package/dist/Form/Hidden.torp +4 -12
- package/dist/Form/Input.torp +38 -32
- package/dist/Form/Label.torp +3 -3
- package/dist/Form/Message.torp +13 -5
- package/dist/Form/MessageSummary.torp +39 -0
- package/dist/Form/Option.torp +11 -7
- package/dist/Form/Select.torp +32 -28
- package/dist/Form/TextArea.torp +35 -31
- package/dist/Form/Toggle.torp +109 -0
- package/dist/Form/index.d.ts +198 -77
- package/dist/Form/index.js +16 -46
- package/dist/ListBox/ListBox.torp +374 -0
- package/dist/ListBox/ListBoxItem.torp +131 -0
- package/dist/ListBox/ListBoxSeparator.torp +29 -0
- package/dist/ListBox/ListBoxTypes.ts +41 -0
- package/dist/ListBox/LoadedListBoxItems.torp +41 -0
- package/dist/ListBox/index.d.ts +136 -0
- package/dist/ListBox/index.js +4 -0
- package/dist/MaskedInput/MaskedInput.torp +143 -0
- package/dist/MaskedInput/index.d.ts +52 -0
- package/dist/MaskedInput/index.js +2 -0
- package/dist/Menu/Menu.torp +90 -70
- package/dist/Menu/MenuButton.torp +17 -10
- package/dist/Menu/MenuCheck.torp +22 -18
- package/dist/Menu/MenuGroup.torp +4 -4
- package/dist/Menu/MenuIndicator.torp +5 -13
- package/dist/Menu/MenuPopout.torp +20 -50
- package/dist/Menu/MenuPopoutContent.torp +15 -66
- package/dist/Menu/MenuPopoutTrigger.torp +19 -100
- package/dist/Menu/MenuRadio.torp +25 -26
- package/dist/Menu/MenuRadioGroup.torp +25 -68
- package/dist/Menu/MenuSeparator.torp +3 -2
- package/dist/Menu/MenuTypes.ts +12 -21
- package/dist/Menu/index.d.ts +45 -51
- package/dist/Menu/index.js +12 -46
- package/dist/MenuBar/MenuBar.torp +31 -83
- package/dist/MenuBar/MenuBarItem.torp +34 -55
- package/dist/MenuBar/MenuBarItemContent.torp +15 -101
- package/dist/MenuBar/MenuBarItemTrigger.torp +20 -55
- package/dist/MenuBar/MenuBarTypes.ts +13 -12
- package/dist/MenuBar/index.d.ts +17 -19
- package/dist/MenuBar/index.js +5 -18
- package/dist/Modal/Modal.torp +28 -48
- package/dist/Modal/ModalContent.torp +21 -44
- package/dist/Modal/ModalOverlay.torp +4 -4
- package/dist/Modal/ModalTrigger.torp +16 -32
- package/dist/Modal/ModalTypes.ts +6 -17
- package/dist/Modal/index.d.ts +17 -29
- package/dist/Modal/index.js +29 -28
- package/dist/Modal/index.js.map +1 -1
- package/dist/Modal/showPrompt.ts +2 -2
- package/dist/NavMenu/NavMenu.torp +22 -104
- package/dist/NavMenu/NavMenuButton.torp +10 -8
- package/dist/NavMenu/NavMenuElement.torp +6 -7
- package/dist/NavMenu/NavMenuGroup.torp +5 -5
- package/dist/NavMenu/NavMenuGroupHeader.torp +5 -8
- package/dist/NavMenu/NavMenuLink.torp +7 -10
- package/dist/NavMenu/NavMenuPopout.torp +16 -61
- package/dist/NavMenu/NavMenuPopoutContent.torp +14 -73
- package/dist/NavMenu/NavMenuPopoutTrigger.torp +47 -0
- package/dist/NavMenu/NavMenuSeparator.torp +3 -6
- package/dist/NavMenu/NavMenuTypes.ts +6 -15
- package/dist/NavMenu/index.d.ts +55 -55
- package/dist/NavMenu/index.js +11 -42
- package/dist/Notification/Notification.torp +12 -24
- package/dist/Notification/NotificationClose.torp +6 -3
- package/dist/Notification/NotificationContainer.torp +3 -7
- package/dist/Notification/NotificationContent.torp +3 -7
- package/dist/Notification/NotificationTypes.ts +1 -1
- package/dist/Notification/index.d.ts +15 -12
- package/dist/Notification/index.js +22 -32
- package/dist/Notification/index.js.map +1 -1
- package/dist/NumberInput/NumberInput.torp +255 -0
- package/dist/NumberInput/index.d.ts +44 -0
- package/dist/NumberInput/index.js +2 -0
- package/dist/Pagination/Pagination.torp +152 -0
- package/dist/Pagination/PaginationItem.torp +137 -0
- package/dist/Pagination/PaginationTypes.ts +22 -0
- package/dist/Pagination/buildPages.ts +61 -0
- package/dist/Pagination/index.d.ts +63 -0
- package/dist/Pagination/index.js +3 -0
- package/dist/Popover/Popover.torp +39 -52
- package/dist/Popover/PopoverContent.torp +27 -76
- package/dist/Popover/PopoverHover.torp +90 -0
- package/dist/Popover/PopoverTrigger.torp +17 -40
- package/dist/Popover/PopoverTypes.ts +5 -18
- package/dist/Popover/index.d.ts +40 -25
- package/dist/Popover/index.js +18 -18
- package/dist/Popover/index.js.map +1 -1
- package/dist/Progress/Progress.torp +88 -0
- package/dist/Progress/index.d.ts +37 -0
- package/dist/Progress/index.js +2 -0
- package/dist/Rating/Rating.torp +225 -0
- package/dist/Rating/index.d.ts +44 -0
- package/dist/Rating/index.js +2 -0
- package/dist/SegmentedControl/SegmentedControl.torp +233 -0
- package/dist/SegmentedControl/SegmentedControlTypes.ts +26 -0
- package/dist/SegmentedControl/SegmentedItem.torp +78 -0
- package/dist/SegmentedControl/index.d.ts +74 -0
- package/dist/SegmentedControl/index.js +3 -0
- package/dist/SelectBox/SelectBox.torp +190 -0
- package/dist/SelectBox/SelectBoxContent.torp +107 -0
- package/dist/SelectBox/SelectBoxTrigger.torp +141 -0
- package/dist/SelectBox/SelectBoxTypes.ts +33 -0
- package/dist/SelectBox/index.d.ts +112 -0
- package/dist/SelectBox/index.js +5 -0
- package/dist/Slider/Slider.torp +248 -0
- package/dist/Slider/index.d.ts +45 -0
- package/dist/Slider/index.js +2 -0
- package/dist/Splitter/Splitter.torp +156 -0
- package/dist/Splitter/SplitterHandle.torp +161 -0
- package/dist/Splitter/SplitterPane.torp +63 -0
- package/dist/Splitter/SplitterTypes.ts +31 -0
- package/dist/Splitter/index.d.ts +103 -0
- package/dist/Splitter/index.js +4 -0
- package/dist/Stepper/Stepper.torp +146 -0
- package/dist/Stepper/StepperMarker.torp +24 -0
- package/dist/Stepper/StepperNext.torp +44 -0
- package/dist/Stepper/StepperPrevious.torp +44 -0
- package/dist/Stepper/StepperStep.torp +90 -0
- package/dist/Stepper/StepperTypes.ts +30 -0
- package/dist/Stepper/index.d.ts +149 -0
- package/dist/Stepper/index.js +6 -0
- package/dist/TabGroup/TabContent.torp +6 -6
- package/dist/TabGroup/TabGroup.torp +97 -113
- package/dist/TabGroup/TabGroupTypes.ts +3 -3
- package/dist/TabGroup/TabHeader.torp +23 -19
- package/dist/TabGroup/TabList.torp +8 -21
- package/dist/TabGroup/TabTrigger.torp +6 -7
- package/dist/TabGroup/index.d.ts +26 -26
- package/dist/TabGroup/index.js +6 -22
- package/dist/TagInput/TagInput.torp +378 -0
- package/dist/TagInput/index.d.ts +48 -0
- package/dist/TagInput/index.js +2 -0
- package/dist/TimePicker/TimePicker.torp +462 -0
- package/dist/TimePicker/index.d.ts +53 -0
- package/dist/TimePicker/index.js +2 -0
- package/dist/ToolBar/ToolBar.torp +117 -0
- package/dist/ToolBar/ToolBarButton.torp +56 -0
- package/dist/ToolBar/ToolBarElement.torp +51 -0
- package/dist/ToolBar/ToolBarPopout.torp +45 -0
- package/dist/ToolBar/ToolBarPopoutContent.torp +34 -0
- package/dist/ToolBar/ToolBarPopoutTrigger.torp +47 -0
- package/dist/ToolBar/ToolBarSeparator.torp +21 -0
- package/dist/ToolBar/ToolBarTypes.ts +28 -0
- package/dist/ToolBar/index.d.ts +210 -0
- package/dist/ToolBar/index.js +8 -0
- package/dist/Tree/Tree.torp +395 -0
- package/dist/Tree/TreeItem.torp +176 -0
- package/dist/Tree/TreeItemChildren.torp +60 -0
- package/dist/Tree/TreeItemContent.torp +46 -0
- package/dist/Tree/TreeItemLabel.torp +94 -0
- package/dist/Tree/TreeItemTrigger.torp +78 -0
- package/dist/Tree/TreeLoadedChildren.torp +122 -0
- package/dist/Tree/TreeTypes.ts +63 -0
- package/dist/Tree/index.d.ts +262 -0
- package/dist/Tree/index.js +7 -0
- package/dist/icons/ChevronDoubleLeft.torp +19 -0
- package/dist/icons/ChevronDoubleRight.torp +19 -0
- package/dist/icons/index.d.ts +61 -1
- package/dist/icons/index.js +17 -58
- package/dist/loader-DEeG5vFY.js +44 -0
- package/dist/loader-DEeG5vFY.js.map +1 -0
- package/dist/motion/index.d.ts +12 -8
- package/dist/motion/index.d.ts.map +1 -0
- package/dist/motion/index.js +72 -88
- package/dist/motion/index.js.map +1 -1
- package/dist/mount/draggable.ts +63 -0
- package/dist/mount/dropTarget.ts +108 -0
- package/dist/mount/hoverTrigger.ts +1 -1
- package/dist/utils/$handle.ts +23 -0
- package/dist/utils/LoadedItems.torp +47 -0
- package/dist/utils/PopoutClickTrigger.torp +99 -0
- package/dist/utils/PopoutTypes.ts +82 -0
- package/dist/utils/SubmenuPopout.torp +89 -0
- package/dist/utils/SubmenuPopoutContent.torp +87 -0
- package/dist/utils/SubmenuPopoutTrigger.torp +160 -0
- package/dist/utils/chartColors.ts +13 -13
- package/dist/utils/chartLayout.ts +143 -0
- package/dist/utils/chartUtils.ts +1 -1
- package/dist/utils/colorUtils.ts +103 -0
- package/dist/utils/createItemGroup.ts +152 -0
- package/dist/utils/dateUtils.ts +8 -7
- package/dist/utils/documentEvents.ts +4 -4
- package/dist/utils/dragState.ts +86 -0
- package/dist/utils/focusGroup.ts +204 -0
- package/dist/utils/focusItem.ts +44 -0
- package/dist/utils/formField.ts +70 -0
- package/dist/utils/hoverOpen.ts +50 -0
- package/dist/utils/hoverReveal.ts +194 -0
- package/dist/utils/loader.ts +113 -0
- package/dist/utils/maskUtils.ts +98 -0
- package/dist/utils/popoutContent.ts +137 -0
- package/dist/utils/reorderList.ts +52 -0
- package/package.json +116 -25
- package/dist/Accordion/index.js.map +0 -1
- package/dist/Breadcrumb/index.js.map +0 -1
- package/dist/Calendar/index.js.map +0 -1
- package/dist/Chart/BarChart.torp +0 -159
- package/dist/Chart/LineChart.torp +0 -196
- package/dist/Chart/index.d.ts +0 -220
- package/dist/Chart/index.js +0 -34
- package/dist/Chart/index.js.map +0 -1
- package/dist/Contextual/index.js.map +0 -1
- package/dist/Dialog/index.js.map +0 -1
- package/dist/Disclosure/index.js.map +0 -1
- package/dist/Drawer/index.js.map +0 -1
- package/dist/Form/index.js.map +0 -1
- package/dist/Menu/index.js.map +0 -1
- package/dist/MenuBar/index.js.map +0 -1
- package/dist/NavMenu/NavMenuPopoutButton.torp +0 -129
- package/dist/NavMenu/index.js.map +0 -1
- package/dist/TabGroup/index.js.map +0 -1
- package/dist/icons/index.js.map +0 -1
- package/dist/index.d.ts +0 -19
- package/dist/metafile-esm.json +0 -1
- package/dist/motion/fade.ts +0 -16
- package/dist/motion/grow.ts +0 -23
- package/dist/motion/measure.ts +0 -28
- package/dist/motion/slide.ts +0 -45
- package/dist/mount/index.d.ts +0 -2
- package/dist/utils/index.d.ts +0 -2
- /package/dist/{Chart → Charts}/DataSeries.ts +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { $watch } from "@torpor/view";
|
|
2
|
+
import { focusItem, type FocusTarget } from "./focusItem";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A managed collection of focusable items with roving tabindex semantics,
|
|
6
|
+
* for group-style components where tabbing into the group lands on one item
|
|
7
|
+
* (the tab stop) and arrow keys move focus within it (ToolBar, MenuBar,
|
|
8
|
+
* button groups, Rating, SegmentedControl...).
|
|
9
|
+
*
|
|
10
|
+
* Owns the registration ledger (indexed states, ordered add/remove) that
|
|
11
|
+
* every list-like component duplicates today, plus the focus bookkeeping:
|
|
12
|
+
* `focusIndex` tracking via onItemFocus, first-item focus when the group
|
|
13
|
+
* itself takes focus, and keyboard movement over enabled items through
|
|
14
|
+
* utils/focusItem.
|
|
15
|
+
*
|
|
16
|
+
* Items are plain states carrying an index, a setFocused implementation and
|
|
17
|
+
* a disabled flag; each component renders its own elements and wires
|
|
18
|
+
* onfocus/onkeydown to the returned handlers.
|
|
19
|
+
*/
|
|
20
|
+
export interface FocusItemState {
|
|
21
|
+
/** Assigned by the group in registration order */
|
|
22
|
+
index?: number;
|
|
23
|
+
/** Focuses this item's element; implemented by the component's item */
|
|
24
|
+
setFocused: () => void;
|
|
25
|
+
/** Disabled items are skipped by movement */
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FocusGroup<T extends FocusItemState> {
|
|
30
|
+
/** The registered item states, in order */
|
|
31
|
+
itemStates: T[];
|
|
32
|
+
|
|
33
|
+
/** Adds an item state, assigning its index from position; returns it */
|
|
34
|
+
registerItem: (item: T) => { index: number };
|
|
35
|
+
|
|
36
|
+
/** Removes an item by index, reindexing the rest */
|
|
37
|
+
removeItem: (itemIndex: number) => void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Item onfocus handler: records the current tab stop
|
|
41
|
+
*/
|
|
42
|
+
handleItemFocus: (index: number) => void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Group-level key handler for a directional press at `index`. Handles
|
|
46
|
+
* left/right (+up/down when vertical), Home and End; arrow keys depend
|
|
47
|
+
* on orientation, matching ToolBar behavior.
|
|
48
|
+
*/
|
|
49
|
+
handleGroupKey: (index: number, e: KeyboardEvent, options?: { vertical?: boolean }) => boolean;
|
|
50
|
+
|
|
51
|
+
/** Moves focus from `index` to the target item (disabled items skipped) */
|
|
52
|
+
moveFocus: (index: number, target: FocusTarget) => void;
|
|
53
|
+
|
|
54
|
+
/** Focuses the first / last enabled item (popout open behavior) */
|
|
55
|
+
focusFirstItem: () => void;
|
|
56
|
+
focusLastItem: () => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether any item is the current tab stop; components derive their
|
|
60
|
+
* per-item tabindex from this plus the tracked focus index
|
|
61
|
+
*/
|
|
62
|
+
getFocusIndex: () => number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether item `index` should be the group's single tab stop: when focus
|
|
66
|
+
* hasn't entered the group yet that is item 0, otherwise whichever item
|
|
67
|
+
* focus last rested on.
|
|
68
|
+
*/
|
|
69
|
+
isTabStop: (index: number) => boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createFocusGroup<T extends FocusItemState>(): FocusGroup<T> {
|
|
73
|
+
const itemStates: T[] = [];
|
|
74
|
+
// The tracked index is reactive, so components can derive each item's
|
|
75
|
+
// roving tabindex from it in their templates
|
|
76
|
+
const $state = $watch({ index: -1 });
|
|
77
|
+
|
|
78
|
+
function registerItem(item: T): { index: number } {
|
|
79
|
+
item.index = itemStates.length;
|
|
80
|
+
itemStates.push(item);
|
|
81
|
+
return { index: item.index };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeItem(itemIndex: number) {
|
|
85
|
+
const i = itemStates.findIndex((item) => item.index === itemIndex);
|
|
86
|
+
if (i !== -1) {
|
|
87
|
+
itemStates.splice(i, 1);
|
|
88
|
+
// Reindex the tail so indices stay dense
|
|
89
|
+
for (let j = i; j < itemStates.length; j++) {
|
|
90
|
+
itemStates[j].index = j;
|
|
91
|
+
}
|
|
92
|
+
if ($state.index > i) {
|
|
93
|
+
$state.index--;
|
|
94
|
+
} else if ($state.index === i) {
|
|
95
|
+
$state.index = -1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleItemFocus(index: number) {
|
|
101
|
+
$state.index = index;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getFocusIndex(): number {
|
|
105
|
+
return $state.index;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isTabStop(index: number): boolean {
|
|
109
|
+
return ($state.index === -1 ? 0 : $state.index) === index;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function moveFocus(index: number, target: FocusTarget) {
|
|
113
|
+
if (itemStates.length === 0) return;
|
|
114
|
+
$state.index = focusItem(itemStates, index === -1 ? 0 : index, target);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function focusFirstItem() {
|
|
118
|
+
if (itemStates.length) {
|
|
119
|
+
$state.index = 0;
|
|
120
|
+
itemStates[0].setFocused();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function focusLastItem() {
|
|
125
|
+
if (itemStates.length > 0) {
|
|
126
|
+
$state.index = itemStates.length - 1;
|
|
127
|
+
itemStates.at(-1)!.setFocused();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleGroupKey(
|
|
132
|
+
index: number,
|
|
133
|
+
e: KeyboardEvent,
|
|
134
|
+
options?: { vertical?: boolean },
|
|
135
|
+
): boolean {
|
|
136
|
+
const vertical = options?.vertical ?? false;
|
|
137
|
+
let handled = true;
|
|
138
|
+
switch (e.key) {
|
|
139
|
+
case "ArrowRight": {
|
|
140
|
+
if (!vertical) {
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
moveFocus(index, "next");
|
|
143
|
+
} else {
|
|
144
|
+
handled = false;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "ArrowLeft": {
|
|
149
|
+
if (!vertical) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
moveFocus(index, "previous");
|
|
152
|
+
} else {
|
|
153
|
+
handled = false;
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "ArrowDown": {
|
|
158
|
+
if (vertical) {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
moveFocus(index, "next");
|
|
161
|
+
} else {
|
|
162
|
+
handled = false;
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "ArrowUp": {
|
|
167
|
+
if (vertical) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
moveFocus(index, "previous");
|
|
170
|
+
} else {
|
|
171
|
+
handled = false;
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "Home": {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
moveFocus(index, "start");
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "End": {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
moveFocus(index, "end");
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
default: {
|
|
186
|
+
handled = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return handled;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
itemStates,
|
|
194
|
+
registerItem,
|
|
195
|
+
removeItem,
|
|
196
|
+
handleItemFocus,
|
|
197
|
+
handleGroupKey,
|
|
198
|
+
moveFocus,
|
|
199
|
+
focusFirstItem,
|
|
200
|
+
focusLastItem,
|
|
201
|
+
getFocusIndex,
|
|
202
|
+
isTabStop,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type FocusTarget = "start" | "previous" | "next" | "end";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standard keyboard navigation for a list of registered items: moves through
|
|
5
|
+
* the items in the given direction and focuses the first one that isn't
|
|
6
|
+
* disabled. Returns the new focus index, or the original when no focusable
|
|
7
|
+
* item was found.
|
|
8
|
+
*/
|
|
9
|
+
export function focusItem<T extends { setFocused: () => void; disabled?: boolean }>(
|
|
10
|
+
items: T[],
|
|
11
|
+
current: number,
|
|
12
|
+
target: FocusTarget,
|
|
13
|
+
): number {
|
|
14
|
+
if (items.length === 0) return current;
|
|
15
|
+
let index = current;
|
|
16
|
+
switch (target) {
|
|
17
|
+
case "start": {
|
|
18
|
+
index = 0;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case "previous": {
|
|
22
|
+
index = Math.max(0, current - 1);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "next": {
|
|
26
|
+
index = Math.min(items.length - 1, current + 1);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "end": {
|
|
30
|
+
index = items.length - 1;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Skip disabled items, searching in the direction of travel
|
|
35
|
+
const step = target === "previous" ? -1 : 1;
|
|
36
|
+
while (index >= 0 && index < items.length && items[index].disabled) {
|
|
37
|
+
index += step;
|
|
38
|
+
}
|
|
39
|
+
if (index < 0 || index >= items.length) {
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
items[index].setFocused();
|
|
43
|
+
return index;
|
|
44
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { FieldContext, FormContext } from "../Form/FormTypes";
|
|
2
|
+
|
|
3
|
+
export interface FormField {
|
|
4
|
+
/** The resolved form data field name: the component's name, or the enclosing Field's */
|
|
5
|
+
name: string | undefined;
|
|
6
|
+
/** Whether this component is contained within a Form Field */
|
|
7
|
+
inField: boolean;
|
|
8
|
+
/** Whether the Field's value is valid; always true outside a Field */
|
|
9
|
+
valid: boolean;
|
|
10
|
+
/** The ID of the Field's Message component, for aria-describedby */
|
|
11
|
+
messageId: string | undefined;
|
|
12
|
+
/** Called after the value has been changed by user interaction */
|
|
13
|
+
handleInput: () => void;
|
|
14
|
+
/** Called when the component loses focus */
|
|
15
|
+
handleBlur: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wires a standalone input component into an enclosing Form and Field, in
|
|
20
|
+
* the same way as the components in the Form family: the Field's name is
|
|
21
|
+
* used when the component doesn't set one, validation runs as the user
|
|
22
|
+
* interacts, and the standard validity attributes can be applied.
|
|
23
|
+
*
|
|
24
|
+
* Usage inside a component:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const form = createFormField(
|
|
28
|
+
* $props?.name,
|
|
29
|
+
* $context[FormContextName] as FormContext | undefined,
|
|
30
|
+
* $context[FieldContextName] as FieldContext | undefined,
|
|
31
|
+
* );
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export default function createFormField(
|
|
35
|
+
name: string | undefined,
|
|
36
|
+
formContext: FormContext | undefined,
|
|
37
|
+
fieldContext: FieldContext | undefined,
|
|
38
|
+
): FormField {
|
|
39
|
+
let blurred = false;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
get name() {
|
|
43
|
+
return name ?? fieldContext?.name;
|
|
44
|
+
},
|
|
45
|
+
get inField(): boolean {
|
|
46
|
+
return fieldContext !== undefined;
|
|
47
|
+
},
|
|
48
|
+
get valid(): boolean {
|
|
49
|
+
if (!fieldContext) return true;
|
|
50
|
+
return fieldContext.state.valid !== false;
|
|
51
|
+
},
|
|
52
|
+
get messageId(): string | undefined {
|
|
53
|
+
return fieldContext?.state.messageId;
|
|
54
|
+
},
|
|
55
|
+
handleInput() {
|
|
56
|
+
if (formContext) {
|
|
57
|
+
formContext.validate();
|
|
58
|
+
}
|
|
59
|
+
if (fieldContext && blurred) {
|
|
60
|
+
fieldContext.validate();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
handleBlur() {
|
|
64
|
+
if (fieldContext) {
|
|
65
|
+
fieldContext.validate();
|
|
66
|
+
}
|
|
67
|
+
blurred = true;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hover-to-open behavior for popout triggers: opens after a delay,
|
|
3
|
+
* resetting the timer as the user moves within the element.
|
|
4
|
+
*/
|
|
5
|
+
export function createHoverOpen(
|
|
6
|
+
open: () => void,
|
|
7
|
+
disabled: () => boolean,
|
|
8
|
+
delay: number,
|
|
9
|
+
): {
|
|
10
|
+
handleMouseEnter: () => void;
|
|
11
|
+
handleMouseLeave: () => void;
|
|
12
|
+
handleMouseMove: () => void;
|
|
13
|
+
} {
|
|
14
|
+
let interval: ReturnType<typeof setTimeout> | undefined;
|
|
15
|
+
|
|
16
|
+
function schedule() {
|
|
17
|
+
interval = setTimeout(() => open(), delay);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handleMouseEnter() {
|
|
21
|
+
if (disabled()) return;
|
|
22
|
+
|
|
23
|
+
// Handle the hover after a delay
|
|
24
|
+
if (!interval) {
|
|
25
|
+
schedule();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleMouseLeave() {
|
|
30
|
+
if (disabled()) return;
|
|
31
|
+
|
|
32
|
+
// Prevent short hovers or touches from firing the event
|
|
33
|
+
if (interval) {
|
|
34
|
+
clearTimeout(interval);
|
|
35
|
+
interval = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleMouseMove() {
|
|
40
|
+
if (disabled()) return;
|
|
41
|
+
|
|
42
|
+
// Reset the interval on mouse move
|
|
43
|
+
if (interval) {
|
|
44
|
+
clearTimeout(interval);
|
|
45
|
+
schedule();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { handleMouseEnter, handleMouseLeave, handleMouseMove };
|
|
50
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hover-to-reveal behavior for hover-activated popout triggers
|
|
3
|
+
* (PopoverHover, ContextualHover).
|
|
4
|
+
*
|
|
5
|
+
* Unlike press activation, hover needs:
|
|
6
|
+
* - an intent delay before opening, so passing over an element doesn't flash
|
|
7
|
+
* a popout (mirrors createHoverOpen for menu items)
|
|
8
|
+
* - an exit grace period after leaving, so moving the pointer from the
|
|
9
|
+
* trigger into the popout content doesn't close it -- entering the content
|
|
10
|
+
* cancels a pending close entirely
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface HoverRevealOptions {
|
|
14
|
+
/** Shows the popout */
|
|
15
|
+
show: () => void;
|
|
16
|
+
/** Hides the popout */
|
|
17
|
+
hide: () => void;
|
|
18
|
+
/** Whether the popout is currently visible */
|
|
19
|
+
isVisible: () => boolean;
|
|
20
|
+
/** Whether hover interaction is ignored entirely */
|
|
21
|
+
disabled?: () => boolean;
|
|
22
|
+
/** The popout content element, for keeping it open when hovered */
|
|
23
|
+
getContent?: () => HTMLElement | null | undefined;
|
|
24
|
+
/** Milliseconds to wait before opening (default 500; 0 opens immediately) */
|
|
25
|
+
hoverDelay?: number;
|
|
26
|
+
/** Milliseconds to wait after the pointer leaves before closing (default 300) */
|
|
27
|
+
exitGrace?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface HoverRevealHandlers {
|
|
31
|
+
/** Handles the pointer entering the trigger */
|
|
32
|
+
handleEnter: () => void;
|
|
33
|
+
/** Handles the pointer leaving the trigger; closes unless it moved into the content element */
|
|
34
|
+
handleLeave: (e: MouseEvent | FocusEvent) => void;
|
|
35
|
+
/** Handles focus landing on the trigger (opens immediately) */
|
|
36
|
+
handleFocus: () => void;
|
|
37
|
+
/** Handles focus leaving the trigger (closes unless focus moved into the content element) */
|
|
38
|
+
handleBlur: (e: FocusEvent) => void;
|
|
39
|
+
/** Cancels pending open/close timers */
|
|
40
|
+
dispose: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createHoverReveal(options: HoverRevealOptions): HoverRevealHandlers {
|
|
44
|
+
const show = options.show;
|
|
45
|
+
const hide = options.hide;
|
|
46
|
+
const isVisible = options.isVisible;
|
|
47
|
+
const disabled = options.disabled ?? (() => false);
|
|
48
|
+
const getContent = options.getContent ?? (() => undefined);
|
|
49
|
+
const hoverDelay = options.hoverDelay ?? 500;
|
|
50
|
+
const exitGrace = options.exitGrace ?? 300;
|
|
51
|
+
|
|
52
|
+
let openTimer: ReturnType<typeof setTimeout> | undefined;
|
|
53
|
+
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
let suppressFocusOpen = false;
|
|
55
|
+
|
|
56
|
+
function clearTimers() {
|
|
57
|
+
if (openTimer !== undefined) {
|
|
58
|
+
clearTimeout(openTimer);
|
|
59
|
+
openTimer = undefined;
|
|
60
|
+
}
|
|
61
|
+
if (closeTimer !== undefined) {
|
|
62
|
+
clearTimeout(closeTimer);
|
|
63
|
+
closeTimer = undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cancelPendingOpen() {
|
|
68
|
+
if (openTimer !== undefined) {
|
|
69
|
+
clearTimeout(openTimer);
|
|
70
|
+
openTimer = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** While armed, moving into the content cancels the close */
|
|
75
|
+
function scheduleClose() {
|
|
76
|
+
if (closeTimer !== undefined) return;
|
|
77
|
+
|
|
78
|
+
closeTimer = setTimeout(() => {
|
|
79
|
+
closeTimer = undefined;
|
|
80
|
+
stopListening();
|
|
81
|
+
// The content hides with `refocusAnchorOnHide`, which drops
|
|
82
|
+
// focus back onto this trigger -- that is not user intent,
|
|
83
|
+
// so swallow the one focus-driven open that follows
|
|
84
|
+
suppressFocusOpen = true;
|
|
85
|
+
hide();
|
|
86
|
+
}, exitGrace);
|
|
87
|
+
|
|
88
|
+
// Cancel as soon as the pointer or focus lands on the content
|
|
89
|
+
document.addEventListener("mouseover", handleDocumentOver);
|
|
90
|
+
document.addEventListener("focusin", handleDocumentFocusIn);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stopListening() {
|
|
94
|
+
document.removeEventListener("mouseover", handleDocumentOver);
|
|
95
|
+
document.removeEventListener("focusin", handleDocumentFocusIn);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleDocumentFocusIn(e: FocusEvent) {
|
|
99
|
+
if (closeTimer === undefined) {
|
|
100
|
+
stopListening();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (insideContent(e.target)) {
|
|
104
|
+
clearTimeout(closeTimer);
|
|
105
|
+
closeTimer = undefined;
|
|
106
|
+
stopListening();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleDocumentOver(e: MouseEvent) {
|
|
111
|
+
if (closeTimer === undefined) {
|
|
112
|
+
document.removeEventListener("mouseover", handleDocumentOver);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (insideContent(e.target)) {
|
|
116
|
+
clearTimeout(closeTimer);
|
|
117
|
+
closeTimer = undefined;
|
|
118
|
+
document.removeEventListener("mouseover", handleDocumentOver);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function insideContent(target: EventTarget | null): boolean {
|
|
123
|
+
const content = getContent();
|
|
124
|
+
if (!content || !target) return false;
|
|
125
|
+
return content === target || content.contains(target as Node);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
handleEnter() {
|
|
130
|
+
if (disabled()) return;
|
|
131
|
+
|
|
132
|
+
if (closeTimer !== undefined) {
|
|
133
|
+
clearTimeout(closeTimer);
|
|
134
|
+
closeTimer = undefined;
|
|
135
|
+
stopListening();
|
|
136
|
+
}
|
|
137
|
+
if (isVisible()) return;
|
|
138
|
+
|
|
139
|
+
if (hoverDelay <= 0) {
|
|
140
|
+
show();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (openTimer === undefined) {
|
|
144
|
+
openTimer = setTimeout(() => {
|
|
145
|
+
openTimer = undefined;
|
|
146
|
+
show();
|
|
147
|
+
}, hoverDelay);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
handleLeave(_e) {
|
|
152
|
+
if (disabled()) return;
|
|
153
|
+
|
|
154
|
+
// Just a pass-over: cancel the pending open and stay closed
|
|
155
|
+
if (openTimer !== undefined) {
|
|
156
|
+
cancelPendingOpen();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (!isVisible()) return;
|
|
160
|
+
|
|
161
|
+
scheduleClose();
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
handleFocus() {
|
|
165
|
+
if (disabled()) return;
|
|
166
|
+
|
|
167
|
+
if (suppressFocusOpen) {
|
|
168
|
+
suppressFocusOpen = false;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (closeTimer !== undefined) {
|
|
173
|
+
clearTimeout(closeTimer);
|
|
174
|
+
closeTimer = undefined;
|
|
175
|
+
stopListening();
|
|
176
|
+
}
|
|
177
|
+
if (!isVisible()) {
|
|
178
|
+
show();
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
handleBlur(_e) {
|
|
183
|
+
if (disabled()) return;
|
|
184
|
+
if (!isVisible()) return;
|
|
185
|
+
|
|
186
|
+
scheduleClose();
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
dispose() {
|
|
190
|
+
clearTimers();
|
|
191
|
+
stopListening();
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { $async, $watch } from "@torpor/view";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared contract for loading data from the network, used by components that
|
|
5
|
+
* can take either static data or a loader function (DataGrid now, ComboBox
|
|
6
|
+
* auto-complete and others later).
|
|
7
|
+
*
|
|
8
|
+
* A loader receives a request object describing what to fetch -- paging,
|
|
9
|
+
* search text and sorting are standard; components extend it with their own
|
|
10
|
+
* fields -- plus an AbortSignal so stale requests can be cancelled.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The base shape of a load request. Components extend this with their own fields. */
|
|
14
|
+
export interface LoadRequest {
|
|
15
|
+
/** 1-based page number to load */
|
|
16
|
+
page?: number;
|
|
17
|
+
/** Number of items per page */
|
|
18
|
+
pageSize?: number;
|
|
19
|
+
/** Search or filter text entered by the user */
|
|
20
|
+
searchText?: string;
|
|
21
|
+
/** The field to sort by */
|
|
22
|
+
sortBy?: string;
|
|
23
|
+
/** The direction to sort in */
|
|
24
|
+
sortDirection?: "asc" | "desc";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The result of a load: the items for the current request, and optionally
|
|
29
|
+
* the total count (needed for paged components to show a pager).
|
|
30
|
+
*/
|
|
31
|
+
export interface LoadResult<T = any> {
|
|
32
|
+
items: T[];
|
|
33
|
+
/** The total number of items across all pages, if known */
|
|
34
|
+
total?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reads data for a request. May return a promise or a plain result, and may
|
|
39
|
+
* return just an array of items when no total is available.
|
|
40
|
+
*/
|
|
41
|
+
export type Loader<T = any, TRequest extends LoadRequest = LoadRequest> = (
|
|
42
|
+
request: TRequest,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
) => Promise<LoadResult<T> | T[]> | LoadResult<T> | T[];
|
|
45
|
+
|
|
46
|
+
/** Wraps a bare array in a LoadResult so consumers always get the same shape */
|
|
47
|
+
export function normalizeLoadResult<T>(result: LoadResult<T> | T[]): LoadResult<T> {
|
|
48
|
+
return Array.isArray(result) ? { items: result } : result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ItemLoaderOptions<T = any, TRequest extends LoadRequest = LoadRequest> {
|
|
52
|
+
/** The loader function to fetch items */
|
|
53
|
+
load: Loader<T, TRequest>;
|
|
54
|
+
/**
|
|
55
|
+
* Builds the request for the current fetch. Called inside the async
|
|
56
|
+
* getter, so reads of reactive state here re-fetch when they change.
|
|
57
|
+
*/
|
|
58
|
+
getRequest: () => TRequest;
|
|
59
|
+
/** Called after each successful load with the normalized result */
|
|
60
|
+
onload?: (result: LoadResult<T>) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Reactive loading state returned by createItemLoader */
|
|
64
|
+
export interface ItemLoaderState<T = any> {
|
|
65
|
+
/** The full result of the current load; suspends while a fetch is pending */
|
|
66
|
+
readonly result: LoadResult<T> | undefined;
|
|
67
|
+
/** The loaded items; suspends while a fetch is pending */
|
|
68
|
+
readonly items: T[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Shared loading state for components that read items from a network loader
|
|
73
|
+
* (DataGrid, ComboBox auto-complete etc). Read the returned state's `items`
|
|
74
|
+
* or `result` inside an `@await` boundary: while a fetch is pending the read
|
|
75
|
+
* suspends, and when dependencies read inside `getRequest` change the fetch
|
|
76
|
+
* re-runs.
|
|
77
|
+
*
|
|
78
|
+
* For lazy loading (fetch only once some condition is met), gate the whole
|
|
79
|
+
* rendering of the region that reads this loader behind that condition
|
|
80
|
+
* (`@if`), so the `@await` boundary -- and with it the first fetch --
|
|
81
|
+
* comes into existence when the condition flips.
|
|
82
|
+
*
|
|
83
|
+
* ```
|
|
84
|
+
* let $load = createItemLoader({
|
|
85
|
+
* load: $props.load,
|
|
86
|
+
* getRequest: () => ({ searchText: $state.searchText }),
|
|
87
|
+
* });
|
|
88
|
+
* @await { ...$load.items... } with { <span>Loading…</span> }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function createItemLoader<T = any, TRequest extends LoadRequest = LoadRequest>(
|
|
92
|
+
options: ItemLoaderOptions<T, TRequest>,
|
|
93
|
+
): ItemLoaderState<T> {
|
|
94
|
+
return $watch({
|
|
95
|
+
get result(): LoadResult<T> | undefined {
|
|
96
|
+
return $async(() => {
|
|
97
|
+
const promise = Promise.resolve(options.load(options.getRequest())).then(
|
|
98
|
+
normalizeLoadResult,
|
|
99
|
+
);
|
|
100
|
+
// Report successful loads to the consumer; failures surface
|
|
101
|
+
// through the component's error boundary, not this chain
|
|
102
|
+
promise.then(
|
|
103
|
+
(r) => options.onload?.(r),
|
|
104
|
+
() => {},
|
|
105
|
+
);
|
|
106
|
+
return promise as Promise<LoadResult<T>>;
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
get items(): T[] {
|
|
110
|
+
return this.result?.items ?? [];
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|