@torpor/ui 0.0.17 → 0.1.1

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 (150) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/Accordion/Accordion.torp +53 -54
  3. package/dist/Accordion/AccordionContent.torp +5 -5
  4. package/dist/Accordion/AccordionHeader.torp +5 -4
  5. package/dist/Accordion/AccordionItem.torp +13 -8
  6. package/dist/Accordion/AccordionTrigger.torp +5 -5
  7. package/dist/Accordion/AccordionTypes.ts +1 -0
  8. package/dist/Dialog/Dialog.torp +156 -0
  9. package/dist/Dialog/DialogBody.torp +34 -0
  10. package/dist/Dialog/DialogButton.torp +48 -0
  11. package/dist/Dialog/DialogFooter.torp +13 -0
  12. package/dist/Dialog/DialogHeader.torp +34 -0
  13. package/dist/Dialog/DialogTypes.ts +16 -0
  14. package/dist/Dialog/index.cjs +53 -0
  15. package/dist/Dialog/index.cjs.map +1 -0
  16. package/dist/Dialog/index.js +22 -0
  17. package/dist/Dialog/index.js.map +1 -0
  18. package/dist/NavMenu/NavMenu.torp +178 -0
  19. package/dist/NavMenu/NavMenuButton.torp +46 -0
  20. package/dist/NavMenu/NavMenuElement.torp +43 -0
  21. package/dist/NavMenu/NavMenuGroup.torp +30 -0
  22. package/dist/NavMenu/NavMenuGroupHeader.torp +30 -0
  23. package/dist/NavMenu/NavMenuLink.torp +52 -0
  24. package/dist/NavMenu/NavMenuPopout.torp +69 -0
  25. package/dist/NavMenu/NavMenuPopoutButton.torp +120 -0
  26. package/dist/NavMenu/NavMenuPopoutContent.torp +79 -0
  27. package/dist/NavMenu/NavMenuSeparator.torp +14 -0
  28. package/dist/NavMenu/NavMenuTypes.ts +32 -0
  29. package/dist/NavMenu/index.cjs +78 -0
  30. package/dist/NavMenu/index.cjs.map +1 -0
  31. package/dist/NavMenu/index.js +42 -0
  32. package/dist/NavMenu/index.js.map +1 -0
  33. package/dist/Offscreen/Offscreen.torp +93 -0
  34. package/dist/Offscreen/index.cjs +33 -0
  35. package/dist/Offscreen/index.cjs.map +1 -0
  36. package/dist/Offscreen/index.js +6 -0
  37. package/dist/Offscreen/index.js.map +1 -0
  38. package/dist/Popover/Popover.torp +105 -0
  39. package/dist/Popover/PopoverContent.torp +78 -0
  40. package/dist/Popover/PopoverContentAnchored.torp +107 -0
  41. package/dist/Popover/PopoverContentContextual.torp +99 -0
  42. package/dist/Popover/PopoverOverlay.torp +32 -0
  43. package/dist/Popover/PopoverTrigger.torp +29 -0
  44. package/dist/Popover/PopoverTriggerContextual.torp +37 -0
  45. package/dist/Popover/PopoverTriggerHover.torp +38 -0
  46. package/dist/Popover/PopoverTypes.ts +30 -0
  47. package/dist/Popover/index.cjs +222 -0
  48. package/dist/Popover/index.cjs.map +1 -0
  49. package/dist/Popover/index.js +183 -0
  50. package/dist/Popover/index.js.map +1 -0
  51. package/dist/Popover/mountClickPopover.ts +50 -0
  52. package/dist/Popover/mountContextPopover.ts +50 -0
  53. package/dist/Popover/mountHoverPopover.ts +57 -0
  54. package/dist/Popover/showPopover.ts +24 -0
  55. package/dist/Popover/showPopoverPrompt.ts +23 -0
  56. package/dist/TabGroup/TabContent.torp +4 -4
  57. package/dist/TabGroup/TabGroup.torp +64 -62
  58. package/dist/TabGroup/TabGroupTypes.ts +1 -0
  59. package/dist/TabGroup/TabHeader.torp +10 -8
  60. package/dist/TabGroup/TabList.torp +4 -3
  61. package/dist/TabGroup/TabTrigger.torp +6 -6
  62. package/dist/metafile-cjs.json +1 -1
  63. package/dist/metafile-esm.json +1 -1
  64. package/dist/mount/disableScroll.ts +28 -0
  65. package/dist/mount/hoverTrigger.ts +59 -0
  66. package/dist/mount/trapFocus.ts +58 -0
  67. package/dist/utils/documentEvents.ts +17 -0
  68. package/dist/utils/getScrollParent.ts +22 -0
  69. package/dist/utils/setContextPosition.ts +31 -0
  70. package/dist/utils/setPopoverPosition.ts +138 -0
  71. package/package.json +26 -6
  72. package/scripts/postbuild.ts +17 -5
  73. package/src/Accordion/Accordion.torp +53 -54
  74. package/src/Accordion/AccordionContent.torp +5 -5
  75. package/src/Accordion/AccordionHeader.torp +5 -4
  76. package/src/Accordion/AccordionItem.torp +13 -8
  77. package/src/Accordion/AccordionTrigger.torp +5 -5
  78. package/src/Accordion/AccordionTypes.ts +1 -0
  79. package/src/Dialog/Dialog.torp +156 -0
  80. package/src/Dialog/DialogBody.torp +34 -0
  81. package/src/Dialog/DialogButton.torp +48 -0
  82. package/src/Dialog/DialogFooter.torp +13 -0
  83. package/src/Dialog/DialogHeader.torp +34 -0
  84. package/src/Dialog/DialogTypes.ts +16 -0
  85. package/src/Dialog/index.ts +7 -0
  86. package/src/Dialog.ts +7 -0
  87. package/src/NavMenu/NavMenu.torp +178 -0
  88. package/src/NavMenu/NavMenuButton.torp +46 -0
  89. package/src/NavMenu/NavMenuElement.torp +43 -0
  90. package/src/NavMenu/NavMenuGroup.torp +30 -0
  91. package/src/NavMenu/NavMenuGroupHeader.torp +30 -0
  92. package/src/NavMenu/NavMenuLink.torp +52 -0
  93. package/src/NavMenu/NavMenuPopout.torp +69 -0
  94. package/src/NavMenu/NavMenuPopoutButton.torp +120 -0
  95. package/src/NavMenu/NavMenuPopoutContent.torp +79 -0
  96. package/src/NavMenu/NavMenuSeparator.torp +14 -0
  97. package/src/NavMenu/NavMenuTypes.ts +32 -0
  98. package/src/NavMenu/index.ts +23 -0
  99. package/src/NavMenu.ts +23 -0
  100. package/src/Offscreen/Offscreen.torp +93 -0
  101. package/src/Offscreen/index.ts +3 -0
  102. package/src/Popover/Popover.torp +105 -0
  103. package/src/Popover/PopoverContent.torp +78 -0
  104. package/src/Popover/PopoverContentAnchored.torp +107 -0
  105. package/src/Popover/PopoverContentContextual.torp +99 -0
  106. package/src/Popover/PopoverOverlay.torp +32 -0
  107. package/src/Popover/PopoverTrigger.torp +29 -0
  108. package/src/Popover/PopoverTriggerContextual.torp +37 -0
  109. package/src/Popover/PopoverTriggerHover.torp +38 -0
  110. package/src/Popover/PopoverTypes.ts +30 -0
  111. package/src/Popover/index.ts +29 -0
  112. package/src/Popover/mountClickPopover.ts +50 -0
  113. package/src/Popover/mountContextPopover.ts +50 -0
  114. package/src/Popover/mountHoverPopover.ts +57 -0
  115. package/src/Popover/showPopover.ts +24 -0
  116. package/src/Popover/showPopoverPrompt.ts +23 -0
  117. package/src/Popover.ts +29 -0
  118. package/src/TabGroup/TabContent.torp +4 -4
  119. package/src/TabGroup/TabGroup.torp +64 -62
  120. package/src/TabGroup/TabGroupTypes.ts +1 -0
  121. package/src/TabGroup/TabHeader.torp +10 -8
  122. package/src/TabGroup/TabList.torp +4 -3
  123. package/src/TabGroup/TabTrigger.torp +6 -6
  124. package/src/mount/disableScroll.ts +28 -0
  125. package/src/mount/hoverTrigger.ts +59 -0
  126. package/src/mount/trapFocus.ts +58 -0
  127. package/src/utils/documentEvents.ts +17 -0
  128. package/src/utils/getScrollParent.ts +22 -0
  129. package/src/utils/setContextPosition.ts +31 -0
  130. package/src/utils/setPopoverPosition.ts +138 -0
  131. package/test/Accordion/accessibility.test.ts +0 -3
  132. package/test/Accordion/for-items.test.ts +2 -4
  133. package/test/Accordion/keyboard.test.ts +0 -3
  134. package/test/Accordion/multiple-item-toggle.test.ts +3 -5
  135. package/test/Accordion/single-item-toggle.test.ts +3 -5
  136. package/test/Dialog/accessibility-alert.test.ts +40 -0
  137. package/test/Dialog/accessibility-modal.test.ts +52 -0
  138. package/test/Dialog/components/CustomDialog.torp +20 -0
  139. package/test/Dialog/components/DialogAccessibility.torp +31 -0
  140. package/test/Dialog/components/DialogFunction.torp +22 -0
  141. package/test/Dialog/components/DialogVariable.torp +35 -0
  142. package/test/Dialog/function.test.ts +26 -0
  143. package/test/Dialog/variable.test.ts +27 -0
  144. package/test/NavMenu/accessibility.test.ts +97 -0
  145. package/test/NavMenu/basic.test.ts +22 -0
  146. package/test/NavMenu/components/NavMenu.torp +28 -0
  147. package/test/NavMenu/components/NavMenuAccessibility.torp +35 -0
  148. package/test/TabGroup/for-items.test.ts +2 -4
  149. package/test/TabGroup/item-trigger.test.ts +10 -11
  150. package/tsup.config.ts +9 -1
@@ -0,0 +1,30 @@
1
+ import { NavMenuGroupContextName, type NavMenuGroupContext } from "./NavMenuTypes";
2
+
3
+ /**
4
+ * A container for a logical group of navigation menu items.
5
+ *
6
+ * @prop {string | undefined} ariaLabel -- An ARIA label for describing the group to screen readers
7
+ */
8
+ export default function NavMenuGroup($props: any) {
9
+ let $state = $watch({
10
+ headerId: ""
11
+ });
12
+
13
+ // Set the context to pass down to items
14
+ let context: NavMenuGroupContext = {
15
+ state: $state,
16
+ };
17
+ $context[NavMenuGroupContextName] = context;
18
+
19
+ @render {
20
+ <div
21
+ id={$props.id}
22
+ class={`torp-nav-menu-group ${$props.class ?? ""}`.trim()}
23
+ role="group"
24
+ aria-label={$props.ariaLabel}
25
+ aria-labelledby={$state.headerId}
26
+ >
27
+ <:slot />
28
+ </div>
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ import getId from "../utils/getId";
2
+ import { NavMenuGroupContextName, type NavMenuGroupContext } from "./NavMenuTypes";
3
+
4
+ /**
5
+ * The header for a NavMenuGroup.
6
+ *
7
+ * @prop {"h1" | "h2" | "h3" | "h4" | "h5" | "h6"} as -- The type of HTML header to use
8
+ */
9
+ export default function NavMenuGroupHeader($props: any) {
10
+ $props.id ??= getId();
11
+
12
+ const context = $context[NavMenuGroupContextName] as NavMenuGroupContext;
13
+ if (!context) {
14
+ throw new Error("NavMenuGroupHeader must be contained within a NavMenuGroup");
15
+ }
16
+ let $state = context.state;
17
+
18
+ // Set the header ID now that we know what it is
19
+ $state.headerId = $props.id;
20
+
21
+ @render {
22
+ <h4
23
+ @// TODO: self={$props.as}
24
+ id={$props.id}
25
+ class={`torp-nav-menu-group-header ${$props.class ?? ""}`.trim()}
26
+ >
27
+ <:slot />
28
+ </h4>
29
+ }
30
+ }
@@ -0,0 +1,52 @@
1
+
2
+ import { NavMenuContextName, type NavMenuContext } from "./NavMenuTypes";
3
+
4
+ /**
5
+ * A link that is placed in a NavMenu and signals a response by a user.
6
+ *
7
+ * @prop {string} href -- The URL for the menu link
8
+ * @prop {newWindow} boolean -- Whether the menu link should open in a new tab/window
9
+ * @prop {boolean} disabled -- Whether this link is disabled
10
+ */
11
+ export default function NavMenuLink($props: any) {
12
+ $props.newWindow ??= false;
13
+
14
+ /** Set when the item is disabled */
15
+ let dataDisabled = $props.disabled || undefined;
16
+
17
+ let link: HTMLAnchorElement;
18
+
19
+ // Get stuff out of the NavMenuContext
20
+ const menuContext = $context[NavMenuContextName] as NavMenuContext;
21
+ if (!menuContext) {
22
+ throw new Error("NavMenuLink must be contained within a NavMenu");
23
+ }
24
+ const { registerItem, handleItemKey, handleItemFocus } = menuContext;
25
+
26
+ // Register this item with the parent NavMenu and store the index
27
+ const { index } = registerItem(setFocused);
28
+
29
+ function setFocused() {
30
+ link.focus();
31
+ }
32
+
33
+ @render {
34
+ <a
35
+ id={$props.id}
36
+ class={`torp-nav-menu-link ${$props.class ?? ""}`.trim()}
37
+ href={$props.href}
38
+ disabled={$props.disabled}
39
+ target={$props.newWindow ? "_blank" : undefined}
40
+ role="menuitem"
41
+ aria-disabled={$props.disabled ? "true" : undefined}
42
+ data-disabled={dataDisabled}
43
+ onkeydown
44
+ onkeydown={handleItemKey}
45
+ onclick
46
+ onfocus={() => handleItemFocus(index)}
47
+ &ref={link}
48
+ >
49
+ <:slot />
50
+ </a>
51
+ }
52
+ }
@@ -0,0 +1,69 @@
1
+ import { NavMenuPopoutContextName, type NavMenuPopoutContext } from "./NavMenuTypes";
2
+
3
+ /**
4
+ * A popout that is attached to a menu button and can be used to show a submenu.
5
+ *
6
+ * The structure of a NavMenuPopout is:
7
+ *
8
+ * ```
9
+ * <NavMenuPopout>
10
+ * <NavMenuPopoutButton />
11
+ * <NavMenuPopoutContent />
12
+ * </NavMenuPopout>
13
+ * ```
14
+ *
15
+ * @prop {boolean} visible -- Whether the popout is visible
16
+ */
17
+ export default function NavMenuPopout($props: any) {
18
+ $props.visible ??= false;
19
+
20
+ let $state = $watch({
21
+ visible: $props.visible
22
+ });
23
+
24
+ let result: any;
25
+
26
+ // Set the context to pass down to items
27
+ let context: NavMenuPopoutContext = {
28
+ state: $state,
29
+ };
30
+ $context[NavMenuPopoutContextName] = context;
31
+
32
+ // Copy props into state whenever they change
33
+ $run(() => {
34
+ $state.visible = $props.visible ?? false;
35
+ });
36
+
37
+ // Dispatch the toggle event when the popout is opened or closed
38
+ let firstToggle = true;
39
+ $run(() => {
40
+ const ontoggle = $props.ontoggle;
41
+ const onopen = $props.onopen;
42
+ const onclose = $props.onclose;
43
+ const visible = $props.visible;
44
+ if (firstToggle) {
45
+ firstToggle = false;
46
+ } else {
47
+ if (ontoggle) ontoggle(visible);
48
+ if (visible) {
49
+ if (onopen) onopen()
50
+ } else {
51
+ if (onclose) onclose(result);
52
+ }
53
+ }
54
+ });
55
+
56
+ function handleMouseLeave() {
57
+ $state.visible = false;
58
+ }
59
+
60
+ @render {
61
+ <div
62
+ id={$props.id}
63
+ class={`torp-nav-menu-popout ${$props.class ?? ""}`.trim()}
64
+ onmouseleave={handleMouseLeave}
65
+ >
66
+ <:slot />
67
+ </div>
68
+ }
69
+ }
@@ -0,0 +1,120 @@
1
+
2
+ import {
3
+ NavMenuContextName,
4
+ NavMenuPopoutContextName,
5
+ type NavMenuContext,
6
+ type NavMenuPopoutContext,
7
+ } from "./NavMenuTypes";
8
+
9
+ /**
10
+ * A button that is placed in a NavMenu and signals a response by a user.
11
+ *
12
+ * @prop {number} hoverDelay -- The delay before showing the popout, in milliseconds
13
+ * @prop {boolean} disabled -- Whether this button is disabled
14
+ */
15
+ export default function NavMenuPopoutButton($props: any) {
16
+ $props.hoverDelay ??= 500;
17
+
18
+ /** Set when the item is disabled */
19
+ let dataDisabled = $props.disabled || undefined;
20
+
21
+ let button: HTMLButtonElement;
22
+ let interval: NodeJS.Timeout | undefined;
23
+
24
+ // Get stuff out of the NavMenuContext
25
+ const menuContext = $context[NavMenuContextName] as NavMenuContext;
26
+ if (!menuContext) {
27
+ throw new Error("NavMenuPopoutButton must be contained within a NavMenu");
28
+ }
29
+ const { registerItem, handleItemKey, handleItemFocus } = menuContext;
30
+
31
+ // Get stuff out of the NavMenuPopoutContext
32
+ const popoutContext = $context[NavMenuPopoutContextName] as NavMenuPopoutContext;
33
+ if (!popoutContext) {
34
+ throw new Error("NavMenuPopoutButton must be contained within a NavMenuPopout");
35
+ }
36
+ let $state = popoutContext.state;
37
+
38
+ // Register this item with the parent NavMenu and store the index
39
+ const { index } = registerItem(setFocused);
40
+
41
+ // TODO: $mount should be run AFTER the button has been set
42
+ //$mount(() => {
43
+ // // Set the anchor element to the button when it exists
44
+ // popoutContext.anchorElement = button;
45
+ //});
46
+
47
+ function setFocused() {
48
+ button.focus();
49
+ }
50
+
51
+ function handleClick() {
52
+ $state.visible = true;
53
+ }
54
+
55
+ function handlePopoutKey(e: KeyboardEvent) {
56
+ switch (e.key) {
57
+ case "Right":
58
+ case "ArrowRight":
59
+ // TODO: This should be based on what side the menu shows on
60
+ e.preventDefault();
61
+ $state.visible = true;
62
+ break;
63
+ default:
64
+ handleItemKey(e);
65
+ }
66
+ }
67
+
68
+ function handleMouseEnter() {
69
+ if ($props.disabled) return;
70
+
71
+ // Handle the hover after a delay
72
+ if (!interval) {
73
+ interval = setTimeout(() => $state.visible = true, $props.hoverDelay);
74
+ }
75
+ }
76
+
77
+ function handleMouseLeave() {
78
+ if ($props.disabled) return;
79
+
80
+ // Prevent short hovers or touches from firing the event
81
+ if (interval) {
82
+ clearTimeout(interval);
83
+ interval = undefined;
84
+ }
85
+ }
86
+
87
+ function handleMouseMove() {
88
+ if ($props.disabled) return;
89
+
90
+ // Reset the interval on mouse move
91
+ if (interval) {
92
+ clearTimeout(interval);
93
+ interval = setTimeout(() => $state.visible = true, $props.hoverDelay);
94
+ }
95
+ }
96
+
97
+ @render {
98
+ <button
99
+ id={$props.id}
100
+ class={`torp-nav-menu-popout-button ${$props.class ?? ""}`.trim()}
101
+ type="button"
102
+ disabled={$props.disabled}
103
+ role="menuitem"
104
+ aria-haspopup="true"
105
+ aria-expanded={$state.visible.toString()}
106
+ aria-disabled={$props.disabled ? "true" : undefined}
107
+ data-disabled={dataDisabled}
108
+ onkeydown={handlePopoutKey}
109
+ onclick={handleClick}
110
+ onfocus={() => handleItemFocus(index)}
111
+ onmouseenter={handleMouseEnter}
112
+ onmouseleave={handleMouseLeave}
113
+ onmousemove={handleMouseMove}
114
+ &ref={button}
115
+ :onmount={(el) => popoutContext.anchorElement = el }
116
+ >
117
+ <:slot />
118
+ </button>
119
+ }
120
+ }
@@ -0,0 +1,79 @@
1
+
2
+ import getScrollParent from "../utils/getScrollParent";
3
+ import setPopoverPosition from "../utils/setPopoverPosition";
4
+ import { NavMenuPopoutContextName, type NavMenuPopoutContext } from "./NavMenuTypes";
5
+
6
+ /**
7
+ * The content that is displayed for a NavMenuPopout when it is open.
8
+ *
9
+ * @prop {"top" | "right" | "bottom" | "left"} side -- Where the popout will be shown, relative to the popout button
10
+ * @prop {"start" | "center" | "end"} alignment -- How the popout will be aligned, relative to the popout button
11
+ */
12
+ export default function NavMenuPopoutContent($props: any) {
13
+ $props.side ??= "bottom";
14
+ $props.alignment ??= "start";
15
+
16
+ let div: HTMLDivElement;
17
+
18
+ // Get stuff out of the NavMenuPopoutContext
19
+ const context = $context[NavMenuPopoutContextName] as NavMenuPopoutContext;
20
+ if (!context) {
21
+ throw new Error("NavMenuPopoutContent must be contained within a NavMenuPopout");
22
+ }
23
+ let $state = context.state;
24
+
25
+ let first = true;
26
+ $run(() => {
27
+ let visible = $state.visible;
28
+
29
+ if (first) {
30
+ first = false;
31
+ return;
32
+ }
33
+
34
+ if (visible) {
35
+ // Wait for the content div to be shown, set its position, and listen for window resize to re-position
36
+ //await tick();
37
+ setPosition();
38
+ if (context.focusFirstElement) {
39
+ context.focusFirstElement();
40
+ }
41
+ const scrollParent = getScrollParent(context.anchorElement)
42
+ scrollParent.addEventListener("resize", setPosition);
43
+ scrollParent.addEventListener("scroll", setPosition);
44
+ } else {
45
+ // Stop listening for window resize and focus the anchor element per WAI guidelines
46
+ const scrollParent = getScrollParent(context.anchorElement)
47
+ scrollParent.removeEventListener("resize", setPosition);
48
+ scrollParent.removeEventListener("scroll", setPosition);
49
+ if (context.anchorElement?.focus) {
50
+ context.anchorElement.focus();
51
+ }
52
+ }
53
+ });
54
+
55
+ function setPosition() {
56
+ if (!context.anchorElement) {
57
+ throw new Error("NavMenuPopoutContent must have an anchor element");
58
+ }
59
+
60
+ if (div) {
61
+ setPopoverPosition(context.anchorElement, div, $props.side, $props.alignment);
62
+ }
63
+ }
64
+
65
+ @render {
66
+ <div style="display: contents">
67
+ @if ($state.visible) {
68
+ <div
69
+ id={$props.id}
70
+ class={`torp-nav-menu-popout-content ${$props.class ?? ""}`.trim()}
71
+ &ref={div}
72
+ :onmount={(el) => setPopoverPosition(context.anchorElement, el, $props.side, $props.alignment)}
73
+ >
74
+ <:slot />
75
+ </div>
76
+ }
77
+ </div>
78
+ }
79
+ }
@@ -0,0 +1,14 @@
1
+
2
+ /**
3
+ * An element in a menu that separates logical sets of menu items.
4
+ */
5
+ export default function NavMenuSeparator($props: any) {
6
+ @render {
7
+ <div
8
+ id={$props.id}
9
+ class={`torp-nav-menu-separator ${$props.class ?? ""}`.trim()} role="separator"
10
+ >
11
+ <:slot />
12
+ </div>
13
+ }
14
+ }
@@ -0,0 +1,32 @@
1
+ export const NavMenuContextName = "NavMenu";
2
+ export const NavMenuPopoutContextName = "NavMenuPopout";
3
+ export const NavMenuGroupContextName = "NavMenuGroup";
4
+
5
+ export interface NavMenuContext {
6
+ /** Called from a NavMenuLink or NavMenuButton when its button is focused */
7
+ handleItemFocus: (index: number) => void;
8
+
9
+ /** Called from a NavMenuLink or NavMenuButton when a key is pressed */
10
+ handleItemKey: (e: KeyboardEvent) => void;
11
+
12
+ /** Called from each NavMenuLink or NavMenuButton to register itself with this NavMenu */
13
+ registerItem: (setFocused: () => void) => { index: number };
14
+ }
15
+
16
+ export interface NavMenuPopoutContext {
17
+ state: {
18
+ visible: boolean;
19
+ };
20
+ anchorElement?: HTMLElement;
21
+ focusFirstElement?: () => void;
22
+ }
23
+
24
+ export interface NavMenuGroupContext {
25
+ state: {
26
+ headerId: string;
27
+ };
28
+ }
29
+
30
+ export interface ItemState {
31
+ setFocused: () => void;
32
+ }
@@ -0,0 +1,23 @@
1
+ import NavMenu from "./NavMenu.torp";
2
+ import NavMenuButton from "./NavMenuButton.torp";
3
+ import NavMenuElement from "./NavMenuElement.torp";
4
+ import NavMenuGroup from "./NavMenuGroup.torp";
5
+ import NavMenuGroupHeader from "./NavMenuGroupHeader.torp";
6
+ import NavMenuLink from "./NavMenuLink.torp";
7
+ import NavMenuPopout from "./NavMenuPopout.torp";
8
+ import NavMenuPopoutButton from "./NavMenuPopoutButton.torp";
9
+ import NavMenuPopoutContent from "./NavMenuPopoutContent.torp";
10
+ import NavMenuSeparator from "./NavMenuSeparator.torp";
11
+
12
+ export {
13
+ NavMenu,
14
+ NavMenuLink,
15
+ NavMenuButton,
16
+ NavMenuElement,
17
+ NavMenuPopout,
18
+ NavMenuPopoutButton,
19
+ NavMenuPopoutContent,
20
+ NavMenuGroup,
21
+ NavMenuGroupHeader,
22
+ NavMenuSeparator,
23
+ };
package/src/NavMenu.ts ADDED
@@ -0,0 +1,23 @@
1
+ import NavMenu from "./NavMenu/NavMenu.torp";
2
+ import NavMenuButton from "./NavMenu/NavMenuButton.torp";
3
+ import NavMenuElement from "./NavMenu/NavMenuElement.torp";
4
+ import NavMenuGroup from "./NavMenu/NavMenuGroup.torp";
5
+ import NavMenuGroupHeader from "./NavMenu/NavMenuGroupHeader.torp";
6
+ import NavMenuLink from "./NavMenu/NavMenuLink.torp";
7
+ import NavMenuPopout from "./NavMenu/NavMenuPopout.torp";
8
+ import NavMenuPopoutButton from "./NavMenu/NavMenuPopoutButton.torp";
9
+ import NavMenuPopoutContent from "./NavMenu/NavMenuPopoutContent.torp";
10
+ import NavMenuSeparator from "./NavMenu/NavMenuSeparator.torp";
11
+
12
+ export {
13
+ NavMenu,
14
+ NavMenuLink,
15
+ NavMenuButton,
16
+ NavMenuElement,
17
+ NavMenuPopout,
18
+ NavMenuPopoutButton,
19
+ NavMenuPopoutContent,
20
+ NavMenuGroup,
21
+ NavMenuGroupHeader,
22
+ NavMenuSeparator,
23
+ };
@@ -0,0 +1,93 @@
1
+
2
+ export default function Offscreen($props: any) {
3
+ $props.visible ||= true;
4
+ $props.position ||= "left";
5
+
6
+ let offscreen: HTMLElement | undefined;
7
+
8
+ // From https://stackoverflow.com/a/46707458
9
+ function measure(el, fn) {
10
+ let oldVisibility = el.style.visibility;
11
+ let oldPosition = el.style.position;
12
+
13
+ let oldParent = el.parentNode;
14
+ let oldBefore = el.nextNode;
15
+
16
+ el.style.visibility = "hidden";
17
+ el.style.position = "absolute";
18
+
19
+ document.body.appendChild(el);
20
+ let result = fn(el);
21
+ el.parentNode.removeChild(el);
22
+
23
+ oldParent.appendChild(el, oldBefore);
24
+ el.style.visibility = oldVisibility;
25
+ el.style.position = oldPosition;
26
+
27
+ return result;
28
+ }
29
+
30
+ function transitionArgs() {
31
+ if (offscreen) {
32
+ switch ($props.position) {
33
+ case "left": {
34
+ offscreen.style.left = 0;
35
+ const width = measure(offscreen, (el) => el.offsetWidth);
36
+ return [
37
+ { transform: `translateX(-${width}px)` },
38
+ { transform: "translateX(0px)" }
39
+ ];
40
+ }
41
+ case "top": {
42
+ offscreen.style.top = 0;
43
+ const height = measure(offscreen, (el) => el.offsetHeight);
44
+ return [
45
+ { transform: `translateY(-${height}px)` },
46
+ { transform: "translateY(0px)" }
47
+ ];
48
+ }
49
+ case "right": {
50
+ offscreen.style.right = 0;
51
+ const width = measure(offscreen, (el) => el.offsetWidth);
52
+ return [
53
+ { transform: `translateX(${width}px)` },
54
+ { transform: "translateX(0px)" }
55
+ ];
56
+ }
57
+ case "bottom": {
58
+ offscreen.style.bottom = 0;
59
+ const height = measure(offscreen, (el) => el.offsetHeight);
60
+ return [
61
+ { transform: `translateY(${height}px)` },
62
+ { transform: "translateY(0px)" }
63
+ ];
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ function close() {
70
+ $props.visible = false;
71
+ if ($props.onclose) {
72
+ $props.onclose()
73
+ }
74
+ }
75
+
76
+ @render {
77
+ <div style="display: contents">
78
+ @if ($props.visible) {
79
+ <div onclick={close} >
80
+ <div
81
+ id={$props.id}
82
+ class={`torp-offscreen ${$props.class ?? ""}`.trim()}
83
+ style="position: fixed;"
84
+ :transition={transitionArgs()}
85
+ &ref={offscreen}
86
+ >
87
+ <:slot />
88
+ </div>
89
+ </div>
90
+ }
91
+ </div>
92
+ }
93
+ }
@@ -0,0 +1,3 @@
1
+ import Offscreen from "./Offscreen.torp";
2
+
3
+ export { Offscreen };
@@ -0,0 +1,105 @@
1
+ import { PopoverContextName, type Point, type PopoverContext, type PopoverState } from "./PopoverTypes";
2
+
3
+ /**
4
+ * A popover that can be used to show rich content or obtain information from a user.
5
+ *
6
+ * See [the WAI ARIA guidelines for Dialogs](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/).
7
+ *
8
+ * The structure of a Popover is:
9
+ *
10
+ * ```
11
+ * <Popover>
12
+ * <PopoverOverlay /> (optional, if modal)
13
+ * <PopoverTrigger/PopoverTriggerContextual/PopoverTriggerHover /> (optional)
14
+ * <PopoverContent/PopoverContentAnchored/PopoverContentContextual />
15
+ * </Popover>
16
+ * ```
17
+ * @prop {boolean} modal -- Whether the popover is modal. If true, an overlay should be supplied to obscure outside content, and only the popover content will be visible to screen readers
18
+ * @prop {boolean} visible -- Whether the popover is visible
19
+ * @prop {HTMLElement | undefined} anchor -- The element to anchor this popover to, as an alternative to supplying a PopoverTrigger
20
+ * @prop {Point | undefined} position -- The position to display this popover at if it's contextual, as an alternative to supplying a PopoverTriggerContextual
21
+ * @prop {((value: any) => void) | undefined} [callback] -- A function to be called when the user selects a response
22
+ */
23
+ export default function Popover($props: any) {
24
+ let $state: PopoverState = $watch({
25
+ visible: false,
26
+ position: { x: 0, y: 0 }
27
+ })
28
+
29
+ let result: any;
30
+
31
+ // Set the context to pass down to items
32
+ let context: PopoverContext = {
33
+ handleButton,
34
+ handleClickOutside,
35
+ state: $state,
36
+ modal: $props.modal,
37
+ anchorElement: $unwrap($props.anchor),
38
+ };
39
+ $context[PopoverContextName] = context;
40
+
41
+ // Copy props into state whenever they change
42
+ $run(() => {
43
+ $state.visible = $props.visible ?? false;
44
+ $state.position = $props.position ?? { x: 0, y: 0 };
45
+ })
46
+
47
+ // Dispatch the toggle event when the popout is opened or closed
48
+ let firstToggle = true;
49
+ $run(() => {
50
+ const ontoggle = $props.ontoggle;
51
+ const onopen = $props.onopen;
52
+ const onclose = $props.onclose;
53
+ const visible = $state.visible;
54
+ if (firstToggle) {
55
+ firstToggle = false;
56
+ } else {
57
+ if (ontoggle) ontoggle(visible);
58
+ if (visible) {
59
+ if (onopen) onopen()
60
+ } else {
61
+ if (onclose) onclose(result);
62
+ }
63
+ }
64
+ });
65
+
66
+ function handleButton(type: "confirm" | "cancel" | undefined, value?: any) {
67
+ // Get the button's result
68
+ switch (type) {
69
+ case "confirm": {
70
+ result = value || true;
71
+ break;
72
+ }
73
+ case "cancel": {
74
+ result = value || false;
75
+ break;
76
+ }
77
+ default: {
78
+ result = value;
79
+ break;
80
+ }
81
+ }
82
+
83
+ // Hide the popover
84
+ $state.visible = false;
85
+
86
+ // Call the callback
87
+ if ($props.callback) {
88
+ $props.callback(result);
89
+ }
90
+ }
91
+
92
+ function handleClickOutside(e: MouseEvent) {
93
+ e.preventDefault();
94
+ $state.visible = false;
95
+ }
96
+
97
+ @render {
98
+ <div
99
+ id={$props.id}
100
+ class={`torp-popover ${$props.class ?? ""}`.trim()}
101
+ >
102
+ <:slot />
103
+ </div>
104
+ }
105
+ }