@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,78 @@
1
+ import { addDocumentEvent, removeDocumentEvent } from "../utils/documentEvents";
2
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
3
+
4
+ /**
5
+ * The content that is displayed for a Popover when it is open, such as a Dialog
6
+ * in the middle of the screen or a div that slides in from the side.
7
+ */
8
+ export default function PopoverContent($props: any) {
9
+ let div: HTMLDivElement;
10
+
11
+ // Get stuff out of the PopoverContext
12
+ const context = $context[PopoverContextName] as PopoverContext;
13
+ if (!context) {
14
+ throw new Error("PopoverContent must be contained within a Popover");
15
+ }
16
+ let $state = context.state;
17
+
18
+ let closeOnClick = false;
19
+
20
+ let first = true;
21
+ $run(() => {
22
+ let visible = $state.visible;
23
+
24
+ if (first) {
25
+ first = false;
26
+ return;
27
+ }
28
+
29
+ if (visible) {
30
+ // HACK: Because this Popover could be shown with a click, we need to wait for another mousedown
31
+ // before we close on click. Otherwise the click immediately bubbles to the document
32
+ closeOnClick = false;
33
+ addDocumentEvent("mousedown", handleDocumentMouseDown);
34
+ addDocumentEvent("click", handleDocumentClick);
35
+ // Wait for the content div to be shown, set its position, and listen for window resize to re-position
36
+ //await tick();
37
+ if (context.focusFirstElement) {
38
+ context.focusFirstElement();
39
+ }
40
+ } else {
41
+ removeDocumentEvent("mousedown", handleDocumentMouseDown);
42
+ removeDocumentEvent("click", handleDocumentClick);
43
+ // Focus the anchor element per WAI guidelines
44
+ if (context.anchorElement?.focus) {
45
+ context.anchorElement.focus();
46
+ }
47
+ }
48
+ });
49
+
50
+ function handleDocumentMouseDown() {
51
+ closeOnClick = true;
52
+ }
53
+
54
+ function handleDocumentClick(e: MouseEvent) {
55
+ if (
56
+ closeOnClick &&
57
+ div &&
58
+ !div.contains(e.target as HTMLElement) &&
59
+ (!context.anchorElement || !context.anchorElement.contains(e.target as HTMLElement))
60
+ ) {
61
+ context.handleClickOutside!(e);
62
+ }
63
+ }
64
+
65
+ @render {
66
+ <div style="display: contents">
67
+ @if ($state.visible) {
68
+ <div
69
+ id={$props.id}
70
+ class={`torp-popover-content ${$props.class ?? ""}`.trim()}
71
+ &ref={div}
72
+ >
73
+ <:slot />
74
+ </div>
75
+ }
76
+ </div>
77
+ }
78
+ }
@@ -0,0 +1,107 @@
1
+ import { addDocumentEvent, removeDocumentEvent } from "../utils/documentEvents";
2
+ import getScrollParent from "../utils/getScrollParent";
3
+ import setPopoverPosition from "../utils/setPopoverPosition";
4
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
5
+
6
+ /**
7
+ * The content that is displayed for a Popover when it is open, displayed
8
+ * alongside an element.
9
+ *
10
+ * @prop {"top" | "right" | "bottom" | "left"} site -- Where the popover will be
11
+ * shown, relative to the popover button
12
+ * @prop {"start" | "center" | "end"} alignment -- How the popover will be
13
+ * aligned, relative to the popover button
14
+ */
15
+ export default function PopoverContentAnchored($props: any) {
16
+ $props.side ??= "bottom";
17
+ $props.alignment ??= "center";
18
+
19
+ let div: HTMLDivElement;
20
+
21
+ let closeOnClick = false;
22
+
23
+ // Get stuff out of the PopoverContext
24
+ const context = $context[PopoverContextName] as PopoverContext;
25
+ if (!context) {
26
+ throw new Error("PopoverContentAnchored must be contained within a Popover");
27
+ }
28
+ let $state = context.state;
29
+
30
+ let first = true;
31
+ $run(() => {
32
+ let visible = $state.visible;
33
+
34
+ if (first) {
35
+ first = false;
36
+ return;
37
+ }
38
+
39
+ if (visible) {
40
+ // HACK: Because this Popover could be shown with a click, we need to wait for another mousedown
41
+ // before we close on click. Otherwise the click immediately bubbles to the document
42
+ closeOnClick = false;
43
+ addDocumentEvent("mousedown", handleDocumentMouseDown);
44
+ addDocumentEvent("click", handleDocumentClick);
45
+ // Wait for the content div to be shown, set its position, and listen for window resize to re-position
46
+ //await tick();
47
+ setPosition();
48
+ if (context.focusFirstElement) {
49
+ context.focusFirstElement();
50
+ }
51
+ const scrollParent = getScrollParent(context.anchorElement)
52
+ scrollParent.addEventListener("resize", setPosition);
53
+ scrollParent.addEventListener("scroll", setPosition);
54
+ } else {
55
+ removeDocumentEvent("mousedown", handleDocumentMouseDown);
56
+ removeDocumentEvent("click", handleDocumentClick);
57
+ // Stop listening for window resize and focus the anchor element per WAI guidelines
58
+ const scrollParent = getScrollParent(context.anchorElement)
59
+ scrollParent.removeEventListener("resize", setPosition);
60
+ scrollParent.removeEventListener("scroll", setPosition);
61
+ if (context.anchorElement?.focus) {
62
+ context.anchorElement.focus();
63
+ }
64
+ }
65
+ });
66
+
67
+ function handleDocumentMouseDown() {
68
+ closeOnClick = true;
69
+ }
70
+
71
+ function handleDocumentClick(e: MouseEvent) {
72
+ if (
73
+ closeOnClick &&
74
+ div &&
75
+ !div.contains(e.target as HTMLElement) &&
76
+ (!context.anchorElement || !context.anchorElement.contains(e.target as HTMLElement))
77
+ ) {
78
+ e.preventDefault();
79
+ context.handleClickOutside!(e);
80
+ }
81
+ }
82
+
83
+ function setPosition() {
84
+ if (!context.anchorElement) {
85
+ throw new Error("PopoverContentAnchored must have an anchor element");
86
+ }
87
+
88
+ if (div) {
89
+ setPopoverPosition(context.anchorElement, div, $props.side, $props.alignment);
90
+ }
91
+ }
92
+
93
+ @render {
94
+ <div style="display: contents">
95
+ @if ($state.visible) {
96
+ <div
97
+ id={$props.id}
98
+ class={`torp-popover-content-anchored ${$props.class ?? ""}`.trim()}
99
+ &ref={div}
100
+ :onmount={(el) => setPopoverPosition(context.anchorElement, el, $props.side, $props.alignment)}
101
+ >
102
+ <:slot />
103
+ </div>
104
+ }
105
+ </div>
106
+ }
107
+ }
@@ -0,0 +1,99 @@
1
+
2
+ import { addDocumentEvent, removeDocumentEvent } from "../utils/documentEvents";
3
+ import getScrollParent from "../utils/getScrollParent";
4
+ import setContextPosition from "../utils/setContextPosition";
5
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
6
+
7
+ /**
8
+ * The content that is displayed for a Popover when it is open, displayed at the
9
+ * position that the user right-clicked.
10
+ */
11
+ export default function PopoverContentContextual($props: any) {
12
+ let div: HTMLDivElement;
13
+
14
+ let closeOnClick = false;
15
+
16
+ // Get stuff out of the PopoverContext
17
+ const context = $context[PopoverContextName] as PopoverContext;
18
+ if (!context) {
19
+ throw new Error("PopoverContentContextual must be contained within a Popover");
20
+ }
21
+ let $state = context.state;
22
+
23
+ let first = true;
24
+ $run(() => {
25
+ let visible = $state.visible;
26
+
27
+ if (first) {
28
+ first = false;
29
+ return;
30
+ }
31
+
32
+ if (visible) {
33
+ // HACK: Because this Contextual could be shown with a click, we need to wait for another mousedown
34
+ // before we close on click. Otherwise the click immediately bubbles to the document
35
+ closeOnClick = false;
36
+ addDocumentEvent("mousedown", handleDocumentMouseDown);
37
+ addDocumentEvent("click", handleDocumentClick);
38
+ // Wait for the content div to be shown, set its position, and listen for window resize to re-position
39
+ //await tick();
40
+ setPosition();
41
+ if (context.focusFirstElement) {
42
+ context.focusFirstElement();
43
+ }
44
+ const scrollParent = getScrollParent(context.anchorElement)
45
+ scrollParent.addEventListener("resize", setPosition);
46
+ scrollParent.addEventListener("scroll", setPosition);
47
+ } else {
48
+ removeDocumentEvent("mousedown", handleDocumentMouseDown);
49
+ removeDocumentEvent("click", handleDocumentClick);
50
+ // Stop listening for window resize and focus the anchor element per WAI guidelines
51
+ const scrollParent = getScrollParent(context.anchorElement)
52
+ scrollParent.removeEventListener("resize", setPosition);
53
+ scrollParent.removeEventListener("scroll", setPosition);
54
+ if (context.anchorElement?.focus) {
55
+ context.anchorElement.focus();
56
+ }
57
+ }
58
+ });
59
+
60
+ function handleDocumentMouseDown() {
61
+ closeOnClick = true;
62
+ }
63
+
64
+ function handleDocumentClick(e: MouseEvent) {
65
+ if (
66
+ closeOnClick &&
67
+ div &&
68
+ !div.contains(e.target as HTMLElement) &&
69
+ (!context.anchorElement || !context.anchorElement.contains(e.target as HTMLElement))
70
+ ) {
71
+ context.handleClickOutside!(e);
72
+ }
73
+ }
74
+
75
+ function setPosition() {
76
+ if (!context.anchorElement) {
77
+ throw new Error("ContextualContent must have an anchor element");
78
+ }
79
+
80
+ if (div && $state.position) {
81
+ setContextPosition(context.anchorElement, div, $state.position);
82
+ }
83
+ }
84
+
85
+ @render {
86
+ <div style="display: contents">
87
+ @if ($state.visible) {
88
+ <div
89
+ id={$props.id}
90
+ class={`torp-popover-content-contextual ${$props.class ?? ""}`.trim()}
91
+ &ref={div}
92
+ :onmount={(el) => setContextPosition(context.anchorElement, el, $state.position)}
93
+ >
94
+ <:slot />
95
+ </div>
96
+ }
97
+ </div>
98
+ }
99
+ }
@@ -0,0 +1,32 @@
1
+ import { disableScroll } from "../mount/disableScroll";
2
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
3
+
4
+ /**
5
+ * The background overlay for a Popover. Adding an overlay automatically sets the Popover to modal.
6
+ */
7
+ export default function PopoverOverlay($props: any) {
8
+ // Get stuff out of the PopoverContext
9
+ const context = $context[PopoverContextName] as PopoverContext;
10
+ if (!context) {
11
+ throw new Error("PopoverOverlay must be contained within a Popover");
12
+ }
13
+ let $state = context.state;
14
+
15
+ // Any Popover with an overlay is modal
16
+ context.modal = true;
17
+
18
+ @render {
19
+ <div style="display: contents">
20
+ @if ($state.visible) {
21
+ <div
22
+ id={$props.id}
23
+ class={`torp-popover-overlay ${$props.class ?? ""}`.trim()}
24
+ tabindex="-1"
25
+ :onmount={(el) => disableScroll(el)}
26
+ >
27
+ <:slot />
28
+ </div>
29
+ }
30
+ </div>
31
+ }
32
+ }
@@ -0,0 +1,29 @@
1
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
2
+
3
+ /**
4
+ * The button that causes a Popover to appear or disappear.
5
+ */
6
+ export default function PopoverTrigger($props: any) {
7
+ // Get stuff out of the PopoverContext
8
+ const context = $context[PopoverContextName] as PopoverContext;
9
+ if (!context) {
10
+ throw new Error("PopoverTrigger must be contained within a Popover");
11
+ }
12
+ let $state = context.state;
13
+
14
+ function handleClick() {
15
+ $state.visible = !$state.visible;
16
+ }
17
+
18
+ @render {
19
+ <button
20
+ type="button"
21
+ id={$props.id}
22
+ class={`torp-popover-trigger ${$props.class ?? ""}`.trim()}
23
+ onclick={handleClick}
24
+ &ref={context.anchorElement}
25
+ >
26
+ <:slot />
27
+ </button>
28
+ }
29
+ }
@@ -0,0 +1,37 @@
1
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
2
+
3
+ /**
4
+ * The element that causes a Popover to appear or disappear when right-clicked.
5
+ */
6
+ export default function PopoverTriggerContextual($props: any) {
7
+ // Get stuff out of the PopoverContext
8
+ const context = $context[PopoverContextName] as PopoverContext;
9
+ if (!context) {
10
+ throw new Error("PopoverTriggerContextual must be contained within a Popover");
11
+ }
12
+ let $state = context.state;
13
+
14
+ function handleContextual(e: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
15
+ e.preventDefault();
16
+ if ($state.position) {
17
+ // Get the position relative to the element, so we can reposition on scroll
18
+ const rect = e.currentTarget.getBoundingClientRect();
19
+ const x = e.clientX - rect.left;
20
+ const y = e.clientY - rect.top;
21
+ $state.position = { x, y };
22
+ //$state.position = { x: window.scrollX + e.clientX, y: window.scrollY + e.clientY };
23
+ }
24
+ $state.visible = !$state.visible;
25
+ }
26
+
27
+ @render {
28
+ <div
29
+ id={$props.id}
30
+ class={`torp-popover-trigger-contextual ${$props.class ?? ""}`.trim()}
31
+ oncontextmenu={handleContextual}
32
+ &ref={context.anchorElement}
33
+ >
34
+ <:slot />
35
+ </div>
36
+ }
37
+ }
@@ -0,0 +1,38 @@
1
+
2
+ import { PopoverContextName, type PopoverContext } from "./PopoverTypes";
3
+ import hoverTrigger from "../mount/hoverTrigger";
4
+
5
+ /**
6
+ * The div that causes a Popover to appear or disappear when hovered.
7
+ */
8
+ export default function PopoverTriggerHover($props: any) {
9
+ // Get stuff out of the PopoverContext
10
+ const context = $context[PopoverContextName] as PopoverContext;
11
+ if (!context) {
12
+ throw new Error("PopoverTriggerHover must be contained within a Popover");
13
+ }
14
+ let $state = context.state;
15
+
16
+ function handleHover(node, e) {
17
+ if ($state.position) {
18
+ // Get the position relative to the element, so we can reposition on scroll
19
+ const rect = node.getBoundingClientRect();
20
+ const x = e.clientX - rect.left;
21
+ const y = e.clientY - rect.top;
22
+ $state.position = { x, y };
23
+ //$state.position = { x: window.scrollX + e.clientX, y: window.scrollY + e.clientY };
24
+ }
25
+ $state.visible = !$state.visible
26
+ }
27
+
28
+ @render {
29
+ <div
30
+ id={$props.id}
31
+ class={`torp-popover-trigger-hover ${$props.class ?? ""}`.trim()}
32
+ :onmount={(el) => hoverTrigger(el, { onHover: handleHover, onExit: handleHover })}
33
+ &ref={context.anchorElement}
34
+ >
35
+ <:slot />
36
+ </div>
37
+ }
38
+ }
@@ -0,0 +1,30 @@
1
+ export const PopoverContextName = "Popover";
2
+
3
+ /** The shared context for popover components (Contextual, Prompt and Popover) */
4
+ export interface PopoverContext {
5
+ handleButton: (type: "confirm" | "cancel" | undefined, value?: any) => void;
6
+ // HACK: We need to be able to let children (i.e. Dialogs) override the click outside functionality
7
+ // There may be a more elegant way to accomplish this too
8
+ handleClickOutside?: (e: MouseEvent) => void;
9
+ state: PopoverState;
10
+ modal: boolean;
11
+ anchorElement?: HTMLElement;
12
+ // HACK: focusFirstElement is necessary because if we focus the first element e.g. in a Dialog's
13
+ // onMount, it will scroll the page to the focused element and mess up our careful positioning
14
+ // We need to be able to manually set the order to
15
+ // 1. set visible (so we have an element with content)
16
+ // 2. set position (so the element is in the right place)
17
+ // 3. set focus (for accessibility)
18
+ // There may be a more elegant way to accomplish this!
19
+ focusFirstElement?: () => void;
20
+ }
21
+
22
+ export interface PopoverState {
23
+ visible: boolean;
24
+ position: Point;
25
+ }
26
+
27
+ export interface Point {
28
+ x: number;
29
+ y: number;
30
+ }
@@ -0,0 +1,29 @@
1
+ import Popover from "./Popover.torp";
2
+ import PopoverContent from "./PopoverContent.torp";
3
+ import PopoverContentAnchored from "./PopoverContentAnchored.torp";
4
+ import PopoverContentContextual from "./PopoverContentContextual.torp";
5
+ import PopoverOverlay from "./PopoverOverlay.torp";
6
+ import PopoverTrigger from "./PopoverTrigger.torp";
7
+ import PopoverTriggerContextual from "./PopoverTriggerContextual.torp";
8
+ import PopoverTriggerHover from "./PopoverTriggerHover.torp";
9
+ import clickPopover from "./mountClickPopover";
10
+ import contextPopover from "./mountContextPopover";
11
+ import hoverPopover from "./mountHoverPopover";
12
+ import showPopover from "./showPopover";
13
+ import showPopoverPrompt from "./showPopoverPrompt";
14
+
15
+ export {
16
+ Popover,
17
+ PopoverOverlay,
18
+ PopoverTrigger,
19
+ PopoverTriggerHover,
20
+ PopoverTriggerContextual,
21
+ PopoverContentAnchored,
22
+ PopoverContentContextual,
23
+ PopoverContent,
24
+ showPopover,
25
+ showPopoverPrompt,
26
+ clickPopover,
27
+ contextPopover,
28
+ hoverPopover,
29
+ };
@@ -0,0 +1,50 @@
1
+ import type { Component } from "@torpor/view";
2
+ import showPopover from "./showPopover";
3
+
4
+ interface ClickPopoverOptions {
5
+ popover: Component;
6
+ props?: Record<PropertyKey, any>;
7
+ }
8
+
9
+ /**
10
+ * A mount directive for showing a popover when an element is clicked.
11
+ * @param node The element to show the popover for
12
+ * @param options Options for showing the popover, consisting of the popover component and any props
13
+ */
14
+ export default function clickPopover(node: HTMLElement, options: ClickPopoverOptions) {
15
+ const { popover, props } = options;
16
+
17
+ // Add a click event handler to the node
18
+ node.addEventListener("click", handleClick);
19
+
20
+ function handleClick(e: MouseEvent) {
21
+ e.preventDefault();
22
+ show();
23
+ }
24
+
25
+ let div: HTMLDivElement;
26
+
27
+ function show() {
28
+ // Show the popover in a div
29
+ if (div) {
30
+ document.body.removeChild(div);
31
+ }
32
+ div = document.createElement("div");
33
+ document.body.appendChild(div);
34
+
35
+ showPopover(popover, node, props, div);
36
+ }
37
+
38
+ return {
39
+ destroy(): void {
40
+ // Remove the event listener from the node on destroy
41
+ node.removeEventListener("click", handleClick);
42
+
43
+ // Remove the div we added
44
+ // TODO: Should we do this in more places?
45
+ if (div) {
46
+ document.body.removeChild(div);
47
+ }
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,50 @@
1
+ import type { Component } from "@torpor/view";
2
+ import showPopover from "./showPopover";
3
+
4
+ interface ContextPopoverOptions {
5
+ popover: Component;
6
+ props?: Record<PropertyKey, any>;
7
+ }
8
+
9
+ /**
10
+ * A mount directive for showing a popover when the user opens an element's context menu.
11
+ * @param node The element to show the popover for
12
+ * @param options Options for showing the popover, consisting of the popover component and any props
13
+ */
14
+ export default function contextPopover(node: HTMLElement, options: ContextPopoverOptions) {
15
+ const { popover, props } = options;
16
+
17
+ // Add a contextmenu event handler to the node
18
+ node.addEventListener("contextmenu", handleClick);
19
+
20
+ function handleClick(e: MouseEvent) {
21
+ e.preventDefault();
22
+ show();
23
+ }
24
+
25
+ let div: HTMLDivElement;
26
+
27
+ function show() {
28
+ // Show the popover in a div
29
+ if (div) {
30
+ document.body.removeChild(div);
31
+ }
32
+ div = document.createElement("div");
33
+ document.body.appendChild(div);
34
+
35
+ showPopover(popover, node, props, div);
36
+ }
37
+
38
+ return {
39
+ destroy(): void {
40
+ // Remove the event listener from the node on destroy
41
+ node.removeEventListener("contextmenu", handleClick);
42
+
43
+ // Remove the div we added
44
+ // TODO: Should we do this in more places?
45
+ if (div) {
46
+ document.body.removeChild(div);
47
+ }
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,57 @@
1
+ import type { Component } from "@torpor/view";
2
+ import hoverTrigger from "../mount/hoverTrigger";
3
+ import showPopover from "./showPopover";
4
+
5
+ interface HoverPopoverOptions {
6
+ popover: Component;
7
+ props?: Record<PropertyKey, any>;
8
+ hoverDelay?: number;
9
+ touchDelay?: number;
10
+ }
11
+
12
+ /**
13
+ * A mount directive for showing a popover when an element is hovered.
14
+ * @param node The element to show the popover for
15
+ * @param options Options for showing the popover, consisting of the popover component, any props and delays for hover and touch
16
+ */
17
+ export default function hoverPopover(node: HTMLElement, options: HoverPopoverOptions): () => void {
18
+ const { popover, props, hoverDelay, touchDelay } = options;
19
+
20
+ let div: HTMLDivElement | undefined;
21
+
22
+ function onHover(node: HTMLElement, e: MouseEvent | TouchEvent) {
23
+ // Show the popover in a div
24
+ if (div) {
25
+ document.body.removeChild(div);
26
+ }
27
+ div = document.createElement("div");
28
+ document.body.appendChild(div);
29
+
30
+ // Get the position relative to the element, so we can reposition on scroll
31
+ const rect = node.getBoundingClientRect();
32
+ let position: { x: number; y: number };
33
+ if (e.type.startsWith("mouse")) {
34
+ const me = e as MouseEvent;
35
+ position = { x: me.clientX - rect.left, y: me.clientY - rect.top };
36
+ } else {
37
+ const te = e as TouchEvent;
38
+ position = {
39
+ x: te.touches[0].clientX - rect.left,
40
+ y: te.touches[0].clientY - rect.top,
41
+ };
42
+ }
43
+ const allProps = Object.assign({}, props, { anchor: node, position });
44
+
45
+ showPopover(popover, node, allProps, div);
46
+ }
47
+
48
+ function onExit() {
49
+ // Remove the div
50
+ if (div) {
51
+ document.body.removeChild(div);
52
+ div = undefined;
53
+ }
54
+ }
55
+
56
+ return hoverTrigger(node, { onHover, onExit, hoverDelay, touchDelay });
57
+ }
@@ -0,0 +1,24 @@
1
+ // TODO: Allow passing in a variable -- so that you can show an inline popover
2
+ import type { Component } from "@torpor/view";
3
+
4
+ /**
5
+ * Shows a popover component in code.
6
+ * @param popover The popover component
7
+ * @param anchor The element to use as an anchor for the popover component
8
+ * @param props The properties for the popover component
9
+ * @param target The target element to show the popover in
10
+ * @returns The value selected by the user
11
+ */
12
+ export default function showPopover(
13
+ popover: Component,
14
+ anchor: HTMLElement,
15
+ props?: Record<PropertyKey, any>,
16
+ target?: Element,
17
+ ): void {
18
+ // Add the anchor element to the properties
19
+ const allProps = Object.assign({}, props, { anchor });
20
+
21
+ // Create the popover
22
+ // TODO: This needs to clean up after itself, like the mount methods do
23
+ popover(target || anchor.parentElement || document.body, null, allProps);
24
+ }
@@ -0,0 +1,23 @@
1
+ import type { Component } from "@torpor/view";
2
+
3
+ /**
4
+ * Shows a popover component in code, and asynchronously returns the value selected by the user.
5
+ * @param popover The popover component
6
+ * @param props The properties for the popover component
7
+ * @param target The target element to show the popover in
8
+ * @returns The value selected by the user
9
+ */
10
+ export default function showPopoverPrompt(
11
+ popover: Component,
12
+ props?: Record<PropertyKey, any>,
13
+ target?: Element,
14
+ ): Promise<void> {
15
+ return new Promise((resolve) => {
16
+ // Add the promise as the callback
17
+ const allProps = Object.assign({}, props, { callback: resolve });
18
+
19
+ // Create the popover
20
+ // TODO: This needs to clean up after itself, like the use methods do
21
+ popover(target || document.body, null, allProps);
22
+ });
23
+ }