@torpor/ui 0.0.17 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/Accordion/Accordion.torp +53 -54
- package/dist/Accordion/AccordionContent.torp +5 -5
- package/dist/Accordion/AccordionHeader.torp +5 -4
- package/dist/Accordion/AccordionItem.torp +13 -8
- package/dist/Accordion/AccordionTrigger.torp +5 -5
- package/dist/Accordion/AccordionTypes.ts +1 -0
- package/dist/Dialog/Dialog.torp +156 -0
- package/dist/Dialog/DialogBody.torp +34 -0
- package/dist/Dialog/DialogButton.torp +48 -0
- package/dist/Dialog/DialogFooter.torp +13 -0
- package/dist/Dialog/DialogHeader.torp +34 -0
- package/dist/Dialog/DialogTypes.ts +16 -0
- package/dist/Dialog/index.cjs +53 -0
- package/dist/Dialog/index.cjs.map +1 -0
- package/dist/Dialog/index.js +22 -0
- package/dist/Dialog/index.js.map +1 -0
- package/dist/NavMenu/NavMenu.torp +178 -0
- package/dist/NavMenu/NavMenuButton.torp +46 -0
- package/dist/NavMenu/NavMenuElement.torp +43 -0
- package/dist/NavMenu/NavMenuGroup.torp +30 -0
- package/dist/NavMenu/NavMenuGroupHeader.torp +30 -0
- package/dist/NavMenu/NavMenuLink.torp +52 -0
- package/dist/NavMenu/NavMenuPopout.torp +69 -0
- package/dist/NavMenu/NavMenuPopoutButton.torp +120 -0
- package/dist/NavMenu/NavMenuPopoutContent.torp +79 -0
- package/dist/NavMenu/NavMenuSeparator.torp +14 -0
- package/dist/NavMenu/NavMenuTypes.ts +32 -0
- package/dist/NavMenu/index.cjs +78 -0
- package/dist/NavMenu/index.cjs.map +1 -0
- package/dist/NavMenu/index.js +42 -0
- package/dist/NavMenu/index.js.map +1 -0
- package/dist/Popover/Popover.torp +105 -0
- package/dist/Popover/PopoverContentAnchored.torp +107 -0
- package/dist/Popover/PopoverContentContextual.torp +99 -0
- package/dist/Popover/PopoverContentStatic.torp +78 -0
- package/dist/Popover/PopoverOverlay.torp +32 -0
- package/dist/Popover/PopoverTrigger.torp +29 -0
- package/dist/Popover/PopoverTriggerContextual.torp +37 -0
- package/dist/Popover/PopoverTriggerHover.torp +30 -0
- package/dist/Popover/PopoverTypes.ts +30 -0
- package/dist/Popover/index.cjs +222 -0
- package/dist/Popover/index.cjs.map +1 -0
- package/dist/Popover/index.js +183 -0
- package/dist/Popover/index.js.map +1 -0
- package/dist/Popover/mountClickPopover.ts +50 -0
- package/dist/Popover/mountContextPopover.ts +50 -0
- package/dist/Popover/mountHoverPopover.ts +57 -0
- package/dist/Popover/showPopover.ts +24 -0
- package/dist/Popover/showPopoverPrompt.ts +23 -0
- package/dist/TabGroup/TabContent.torp +4 -4
- package/dist/TabGroup/TabGroup.torp +58 -59
- package/dist/TabGroup/TabGroupTypes.ts +1 -0
- package/dist/TabGroup/TabHeader.torp +10 -8
- package/dist/TabGroup/TabList.torp +4 -3
- package/dist/TabGroup/TabTrigger.torp +6 -6
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/mount/disableScroll.ts +28 -0
- package/dist/mount/hoverTrigger.ts +59 -0
- package/dist/mount/trapFocus.ts +58 -0
- package/dist/utils/documentEvents.ts +17 -0
- package/dist/utils/getScrollParent.ts +22 -0
- package/dist/utils/setContextPosition.ts +31 -0
- package/dist/utils/setPopoverPosition.ts +138 -0
- package/package.json +21 -6
- package/scripts/postbuild.ts +17 -5
- package/src/Accordion/Accordion.torp +53 -54
- package/src/Accordion/AccordionContent.torp +5 -5
- package/src/Accordion/AccordionHeader.torp +5 -4
- package/src/Accordion/AccordionItem.torp +13 -8
- package/src/Accordion/AccordionTrigger.torp +5 -5
- package/src/Accordion/AccordionTypes.ts +1 -0
- package/src/Dialog/Dialog.torp +156 -0
- package/src/Dialog/DialogBody.torp +34 -0
- package/src/Dialog/DialogButton.torp +48 -0
- package/src/Dialog/DialogFooter.torp +13 -0
- package/src/Dialog/DialogHeader.torp +34 -0
- package/src/Dialog/DialogTypes.ts +16 -0
- package/src/Dialog/index.ts +7 -0
- package/src/Dialog.ts +7 -0
- package/src/NavMenu/NavMenu.torp +178 -0
- package/src/NavMenu/NavMenuButton.torp +46 -0
- package/src/NavMenu/NavMenuElement.torp +43 -0
- package/src/NavMenu/NavMenuGroup.torp +30 -0
- package/src/NavMenu/NavMenuGroupHeader.torp +30 -0
- package/src/NavMenu/NavMenuLink.torp +52 -0
- package/src/NavMenu/NavMenuPopout.torp +69 -0
- package/src/NavMenu/NavMenuPopoutButton.torp +120 -0
- package/src/NavMenu/NavMenuPopoutContent.torp +79 -0
- package/src/NavMenu/NavMenuSeparator.torp +14 -0
- package/src/NavMenu/NavMenuTypes.ts +32 -0
- package/src/NavMenu/index.ts +23 -0
- package/src/NavMenu.ts +23 -0
- package/src/Popover/Popover.torp +105 -0
- package/src/Popover/PopoverContentAnchored.torp +107 -0
- package/src/Popover/PopoverContentContextual.torp +99 -0
- package/src/Popover/PopoverContentStatic.torp +78 -0
- package/src/Popover/PopoverOverlay.torp +32 -0
- package/src/Popover/PopoverTrigger.torp +29 -0
- package/src/Popover/PopoverTriggerContextual.torp +37 -0
- package/src/Popover/PopoverTriggerHover.torp +30 -0
- package/src/Popover/PopoverTypes.ts +30 -0
- package/src/Popover/index.ts +29 -0
- package/src/Popover/mountClickPopover.ts +50 -0
- package/src/Popover/mountContextPopover.ts +50 -0
- package/src/Popover/mountHoverPopover.ts +57 -0
- package/src/Popover/showPopover.ts +24 -0
- package/src/Popover/showPopoverPrompt.ts +23 -0
- package/src/Popover.ts +29 -0
- package/src/TabGroup/TabContent.torp +4 -4
- package/src/TabGroup/TabGroup.torp +58 -59
- package/src/TabGroup/TabGroupTypes.ts +1 -0
- package/src/TabGroup/TabHeader.torp +10 -8
- package/src/TabGroup/TabList.torp +4 -3
- package/src/TabGroup/TabTrigger.torp +6 -6
- package/src/mount/disableScroll.ts +28 -0
- package/src/mount/hoverTrigger.ts +59 -0
- package/src/mount/trapFocus.ts +58 -0
- package/src/utils/documentEvents.ts +17 -0
- package/src/utils/getScrollParent.ts +22 -0
- package/src/utils/setContextPosition.ts +31 -0
- package/src/utils/setPopoverPosition.ts +138 -0
- package/test/Accordion/accessibility.test.ts +0 -3
- package/test/Accordion/for-items.test.ts +2 -4
- package/test/Accordion/keyboard.test.ts +0 -3
- package/test/Accordion/multiple-item-toggle.test.ts +3 -5
- package/test/Accordion/single-item-toggle.test.ts +3 -5
- package/test/Dialog/accessibility-alert.test.ts +40 -0
- package/test/Dialog/accessibility-modal.test.ts +52 -0
- package/test/Dialog/components/CustomDialog.torp +20 -0
- package/test/Dialog/components/DialogAccessibility.torp +31 -0
- package/test/Dialog/components/DialogFunction.torp +22 -0
- package/test/Dialog/components/DialogVariable.torp +35 -0
- package/test/Dialog/function.test.ts +26 -0
- package/test/Dialog/variable.test.ts +27 -0
- package/test/NavMenu/accessibility.test.ts +97 -0
- package/test/NavMenu/basic.test.ts +22 -0
- package/test/NavMenu/components/NavMenu.torp +28 -0
- package/test/NavMenu/components/NavMenuAccessibility.torp +35 -0
- package/test/TabGroup/for-items.test.ts +2 -4
- package/test/TabGroup/item-trigger.test.ts +3 -5
- package/tsup.config.ts +8 -1
|
@@ -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,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
|
+
* <PopoverContentStatic/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
|
+
}
|
|
@@ -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: content;">
|
|
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("ContextualContent 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: content;">
|
|
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,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 PopoverContentStatic($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("PopoverContentStatic 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: content;">
|
|
67
|
+
@if ($state.visible) {
|
|
68
|
+
<div
|
|
69
|
+
id={$props.id}
|
|
70
|
+
class={`torp-popover-content-static ${$props.class ?? ""}`.trim()}
|
|
71
|
+
&ref={div}
|
|
72
|
+
>
|
|
73
|
+
<:slot />
|
|
74
|
+
</div>
|
|
75
|
+
}
|
|
76
|
+
</div>
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -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: content;">
|
|
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 Contextual");
|
|
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,30 @@
|
|
|
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() {
|
|
17
|
+
$state.visible = !$state.visible
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@render {
|
|
21
|
+
<div
|
|
22
|
+
id={$props.id}
|
|
23
|
+
class={`torp-popover-trigger-hover ${$props.class ?? ""}`.trim()}
|
|
24
|
+
:onmount={(el) => hoverTrigger(el, { onHover: handleHover, onExit: handleHover })}
|
|
25
|
+
&ref={context.anchorElement}
|
|
26
|
+
>
|
|
27
|
+
<:slot />
|
|
28
|
+
</div>
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -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 PopoverContentAnchored from "./PopoverContentAnchored.torp";
|
|
3
|
+
import PopoverContentContextual from "./PopoverContentContextual.torp";
|
|
4
|
+
import PopoverContentStatic from "./PopoverContentStatic.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
|
+
PopoverContentStatic,
|
|
24
|
+
showPopover,
|
|
25
|
+
showPopoverPrompt,
|
|
26
|
+
clickPopover,
|
|
27
|
+
contextPopover,
|
|
28
|
+
hoverPopover,
|
|
29
|
+
};
|