@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.
- package/CHANGELOG.md +41 -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/Offscreen/Offscreen.torp +93 -0
- package/dist/Offscreen/index.cjs +33 -0
- package/dist/Offscreen/index.cjs.map +1 -0
- package/dist/Offscreen/index.js +6 -0
- package/dist/Offscreen/index.js.map +1 -0
- package/dist/Popover/Popover.torp +105 -0
- package/dist/Popover/PopoverContent.torp +78 -0
- package/dist/Popover/PopoverContentAnchored.torp +107 -0
- package/dist/Popover/PopoverContentContextual.torp +99 -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 +38 -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 +64 -62
- 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 +26 -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/Offscreen/Offscreen.torp +93 -0
- package/src/Offscreen/index.ts +3 -0
- package/src/Popover/Popover.torp +105 -0
- package/src/Popover/PopoverContent.torp +78 -0
- package/src/Popover/PopoverContentAnchored.torp +107 -0
- package/src/Popover/PopoverContentContextual.torp +99 -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 +38 -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 +64 -62
- 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 +10 -11
- package/tsup.config.ts +9 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export default function setPopoverPosition(
|
|
2
|
+
anchor: HTMLElement,
|
|
3
|
+
content: HTMLElement,
|
|
4
|
+
side: "top" | "right" | "bottom" | "left",
|
|
5
|
+
alignment: "start" | "center" | "end",
|
|
6
|
+
): void {
|
|
7
|
+
var anchorRect = anchor.getBoundingClientRect();
|
|
8
|
+
var contentRect = content.getBoundingClientRect();
|
|
9
|
+
|
|
10
|
+
content.style.position = "fixed";
|
|
11
|
+
|
|
12
|
+
// Check that we there is enough room to show the content on the side requested
|
|
13
|
+
// Otherwise, if there is enough room on the other side, show it there instead
|
|
14
|
+
let calculatedSide = side;
|
|
15
|
+
const canShowOnTop = anchorRect.top - contentRect.height >= 0;
|
|
16
|
+
const canShowAtRight = anchorRect.right + contentRect.width <= window.innerWidth;
|
|
17
|
+
const canShowOnBottom = anchorRect.bottom + contentRect.height <= window.innerHeight;
|
|
18
|
+
const canShowAtLeft = anchorRect.left - contentRect.width >= 0;
|
|
19
|
+
switch (side) {
|
|
20
|
+
case "top": {
|
|
21
|
+
if (!canShowOnTop && canShowOnBottom) {
|
|
22
|
+
calculatedSide = "bottom";
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
case "right": {
|
|
27
|
+
if (!canShowAtRight && canShowAtLeft) {
|
|
28
|
+
calculatedSide = "left";
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
case "bottom": {
|
|
33
|
+
if (!canShowOnBottom && canShowOnTop) {
|
|
34
|
+
calculatedSide = "top";
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "left": {
|
|
39
|
+
if (!canShowAtLeft && canShowAtRight) {
|
|
40
|
+
calculatedSide = "right";
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default: {
|
|
45
|
+
throw new Error(`Invalid MenuPopout side: ${side}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
switch (calculatedSide) {
|
|
50
|
+
case "top": {
|
|
51
|
+
content.style.top = anchorRect.top - contentRect.height + "px";
|
|
52
|
+
setVerticalAlignment(content, anchorRect, contentRect, alignment);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "right": {
|
|
56
|
+
content.style.left = anchorRect.right + "px";
|
|
57
|
+
setHorizontalAlignment(content, anchorRect, contentRect, alignment);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "bottom": {
|
|
61
|
+
content.style.top = anchorRect.bottom + "px";
|
|
62
|
+
setVerticalAlignment(content, anchorRect, contentRect, alignment);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "left": {
|
|
66
|
+
content.style.left = anchorRect.left - contentRect.width + "px";
|
|
67
|
+
setHorizontalAlignment(content, anchorRect, contentRect, alignment);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
default: {
|
|
71
|
+
throw new Error(`Invalid MenuPopout side: ${side}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setVerticalAlignment(
|
|
77
|
+
content: HTMLElement,
|
|
78
|
+
anchorRect: DOMRect,
|
|
79
|
+
contentRect: DOMRect,
|
|
80
|
+
alignment: "start" | "center" | "end",
|
|
81
|
+
) {
|
|
82
|
+
let left = 0;
|
|
83
|
+
switch (alignment) {
|
|
84
|
+
case "start": {
|
|
85
|
+
left = anchorRect.left;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "center": {
|
|
89
|
+
left = anchorRect.left + anchorRect.width / 2 - contentRect.width / 2;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "end": {
|
|
93
|
+
left = anchorRect.right - contentRect.width;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
default: {
|
|
97
|
+
throw new Error(`Invalid Popout alignment: ${alignment}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Try to make sure the content is entirely on the screen horizontally
|
|
102
|
+
left = Math.min(left, window.innerWidth - contentRect.width);
|
|
103
|
+
left = Math.max(left, 0);
|
|
104
|
+
|
|
105
|
+
content.style.left = left + "px";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setHorizontalAlignment(
|
|
109
|
+
content: HTMLElement,
|
|
110
|
+
anchorRect: DOMRect,
|
|
111
|
+
contentRect: DOMRect,
|
|
112
|
+
alignment: "start" | "center" | "end",
|
|
113
|
+
) {
|
|
114
|
+
let top = 0;
|
|
115
|
+
switch (alignment) {
|
|
116
|
+
case "start": {
|
|
117
|
+
top = anchorRect.top;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "center": {
|
|
121
|
+
top = anchorRect.top + anchorRect.height / 2 - contentRect.height / 2;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case "end": {
|
|
125
|
+
top = anchorRect.bottom - contentRect.height;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
default: {
|
|
129
|
+
throw new Error(`Invalid Popout alignment: ${alignment}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try to make sure the content is entirely on the screen vertically
|
|
134
|
+
top = Math.min(top, window.innerHeight - contentRect.height);
|
|
135
|
+
top = Math.max(top, 0);
|
|
136
|
+
|
|
137
|
+
content.style.top = top + "px";
|
|
138
|
+
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { queryByText } from "@testing-library/dom";
|
|
2
2
|
import "@testing-library/jest-dom/vitest";
|
|
3
|
-
import userEvent from "@testing-library/user-event";
|
|
4
3
|
import { mount } from "@torpor/view";
|
|
5
4
|
import { describe, expect, it } from "vitest";
|
|
6
5
|
import AccordionAccessibility from "./components/AccordionAccessibility.torp";
|
|
7
6
|
|
|
8
7
|
describe("Accordion", () => {
|
|
9
8
|
it("Accessibility", async () => {
|
|
10
|
-
const user = userEvent.setup();
|
|
11
|
-
|
|
12
9
|
const container = document.createElement("div");
|
|
13
10
|
document.body.appendChild(container);
|
|
14
11
|
mount(container, AccordionAccessibility, { value: ["1", "3"] });
|
|
@@ -7,8 +7,6 @@ import AccordionList from "./components/AccordionList.torp";
|
|
|
7
7
|
|
|
8
8
|
describe("Accordion", () => {
|
|
9
9
|
it("Items in a for loop", async () => {
|
|
10
|
-
const user = userEvent.setup();
|
|
11
|
-
|
|
12
10
|
let $state = $watch({
|
|
13
11
|
value: "1",
|
|
14
12
|
items: [
|
|
@@ -35,7 +33,7 @@ describe("Accordion", () => {
|
|
|
35
33
|
expect(queryByText(container, "Content 3")).not.toBeInTheDocument();
|
|
36
34
|
|
|
37
35
|
// Clicking header 1 should reveal body 1
|
|
38
|
-
await
|
|
36
|
+
await userEvent.click(getByText(container, "Item 1"));
|
|
39
37
|
expect(queryByText(container, "Content 1")).toBeInTheDocument();
|
|
40
38
|
|
|
41
39
|
$state.items.push({ id: 3, header: "Item 4", text: "Content 4" });
|
|
@@ -47,7 +45,7 @@ describe("Accordion", () => {
|
|
|
47
45
|
expect(queryByText(container, "Content 4")).not.toBeInTheDocument();
|
|
48
46
|
|
|
49
47
|
// Clicking header 4 should reveal body 4
|
|
50
|
-
await
|
|
48
|
+
await userEvent.click(getByText(container, "Item 4"));
|
|
51
49
|
expect(queryByText(container, "Content 1")).not.toBeInTheDocument();
|
|
52
50
|
expect(queryByText(container, "Content 4")).toBeInTheDocument();
|
|
53
51
|
});
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { fireEvent, getByText, queryByText } from "@testing-library/dom";
|
|
2
2
|
import "@testing-library/jest-dom/vitest";
|
|
3
|
-
import userEvent from "@testing-library/user-event";
|
|
4
3
|
import { mount } from "@torpor/view";
|
|
5
4
|
import { describe, expect, it } from "vitest";
|
|
6
5
|
import AccordionKeyboard from "./components/AccordionKeyboard.torp";
|
|
7
6
|
|
|
8
7
|
describe("Accordion", () => {
|
|
9
8
|
it("Keyboard", async () => {
|
|
10
|
-
const user = userEvent.setup();
|
|
11
|
-
|
|
12
9
|
const container = document.createElement("div");
|
|
13
10
|
document.body.appendChild(container);
|
|
14
11
|
mount(container, AccordionKeyboard, { value: ["1", "2"] });
|
|
@@ -7,8 +7,6 @@ import AccordionMultiple from "./components/AccordionMultiple.torp";
|
|
|
7
7
|
|
|
8
8
|
describe("Accordion", () => {
|
|
9
9
|
it("Multiple item toggle", async () => {
|
|
10
|
-
const user = userEvent.setup();
|
|
11
|
-
|
|
12
10
|
const container = document.createElement("div");
|
|
13
11
|
document.body.appendChild(container);
|
|
14
12
|
mount(container, AccordionMultiple);
|
|
@@ -18,16 +16,16 @@ describe("Accordion", () => {
|
|
|
18
16
|
expect(queryByText(container, "Content 3")).not.toBeInTheDocument();
|
|
19
17
|
|
|
20
18
|
// Clicking header 1 should reveal body 1
|
|
21
|
-
await
|
|
19
|
+
await userEvent.click(getByText(container, "Header 1"));
|
|
22
20
|
expect(queryByText(container, "Content 1")).toBeInTheDocument();
|
|
23
21
|
|
|
24
22
|
// Clicking header 2 should reveal body 1 and body 2
|
|
25
|
-
await
|
|
23
|
+
await userEvent.click(getByText(container, "Header 2"));
|
|
26
24
|
expect(queryByText(container, "Content 1")).toBeInTheDocument();
|
|
27
25
|
expect(queryByText(container, "Content 2")).toBeInTheDocument();
|
|
28
26
|
|
|
29
27
|
// Clicking header 2 again should hide body 2
|
|
30
|
-
await
|
|
28
|
+
await userEvent.click(getByText(container, "Header 2"));
|
|
31
29
|
expect(queryByText(container, "Content 1")).toBeInTheDocument();
|
|
32
30
|
expect(queryByText(container, "Content 2")).not.toBeInTheDocument();
|
|
33
31
|
});
|
|
@@ -7,8 +7,6 @@ import AccordionSingle from "./components/AccordionSingle.torp";
|
|
|
7
7
|
|
|
8
8
|
describe("Accordion", () => {
|
|
9
9
|
it("Single item toggle", async () => {
|
|
10
|
-
const user = userEvent.setup();
|
|
11
|
-
|
|
12
10
|
const container = document.createElement("div");
|
|
13
11
|
document.body.appendChild(container);
|
|
14
12
|
mount(container, AccordionSingle);
|
|
@@ -18,16 +16,16 @@ describe("Accordion", () => {
|
|
|
18
16
|
expect(queryByText(container, "Content 3")).not.toBeInTheDocument();
|
|
19
17
|
|
|
20
18
|
// Clicking header 1 should reveal body 1
|
|
21
|
-
await
|
|
19
|
+
await userEvent.click(getByText(container, "Header 1"));
|
|
22
20
|
expect(queryByText(container, "Content 1")).toBeInTheDocument();
|
|
23
21
|
|
|
24
22
|
// Clicking header 2 should hide body 1 and reveal body 2
|
|
25
|
-
await
|
|
23
|
+
await userEvent.click(getByText(container, "Header 2"));
|
|
26
24
|
expect(queryByText(container, "Content 1")).not.toBeInTheDocument();
|
|
27
25
|
expect(queryByText(container, "Content 2")).toBeInTheDocument();
|
|
28
26
|
|
|
29
27
|
// Clicking header 2 again should hide body 2
|
|
30
|
-
await
|
|
28
|
+
await userEvent.click(getByText(container, "Header 2"));
|
|
31
29
|
expect(queryByText(container, "Content 2")).not.toBeInTheDocument();
|
|
32
30
|
});
|
|
33
31
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { queryByText } from "@testing-library/dom";
|
|
2
|
+
import "@testing-library/jest-dom/vitest";
|
|
3
|
+
import { mount } from "@torpor/view";
|
|
4
|
+
import { assert, describe, expect, it } from "vitest";
|
|
5
|
+
import DialogAccessibility from "./components/DialogAccessibility.torp";
|
|
6
|
+
|
|
7
|
+
describe("Dialog", () => {
|
|
8
|
+
it("Accessibility -- alert", async () => {
|
|
9
|
+
const container = document.createElement("div");
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
mount(container, DialogAccessibility, { alert: true });
|
|
12
|
+
|
|
13
|
+
// Tests from https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/
|
|
14
|
+
// This is for a Dialog with its alert property set to true
|
|
15
|
+
|
|
16
|
+
// The element that contains all elements of the dialog, including the
|
|
17
|
+
// alert message and any dialog buttons, has role alertdialog
|
|
18
|
+
let dialog = queryByText(container, "Which color do you prefer?");
|
|
19
|
+
assert(dialog, "Dialog not found");
|
|
20
|
+
assert(dialog.parentElement, "Dialog parent not found");
|
|
21
|
+
dialog = dialog.parentElement;
|
|
22
|
+
expect(dialog).toHaveAttribute("role", "alertdialog");
|
|
23
|
+
|
|
24
|
+
// The element with role alertdialog has either:
|
|
25
|
+
// * A value for aria-labelledby that refers to the element containing
|
|
26
|
+
// the title of the dialog if the dialog has a visible label
|
|
27
|
+
// * A value for aria-label if the dialog does not have a visible
|
|
28
|
+
// label
|
|
29
|
+
const header = queryByText(container, "Color preference");
|
|
30
|
+
assert(header, "Header not found");
|
|
31
|
+
expect(dialog).toHaveAttribute("aria-labelledby", header.id);
|
|
32
|
+
|
|
33
|
+
// The element with role alertdialog has a value set for
|
|
34
|
+
// aria-describedby that refers to the element containing the alert
|
|
35
|
+
// message
|
|
36
|
+
const body = queryByText(container, "Which color do you prefer?");
|
|
37
|
+
assert(body, "Body not found");
|
|
38
|
+
expect(dialog).toHaveAttribute("aria-describedby", body.id);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { queryByText } from "@testing-library/dom";
|
|
2
|
+
import "@testing-library/jest-dom/vitest";
|
|
3
|
+
import { mount } from "@torpor/view";
|
|
4
|
+
import { assert, describe, expect, it } from "vitest";
|
|
5
|
+
import DialogAccessibility from "./components/DialogAccessibility.torp";
|
|
6
|
+
|
|
7
|
+
describe("Dialog", () => {
|
|
8
|
+
it("Accessibility -- modal", async () => {
|
|
9
|
+
const container = document.createElement("div");
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
mount(container, DialogAccessibility, { modal: true, ariaAnnounce: false });
|
|
12
|
+
|
|
13
|
+
// Tests from https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
|
|
14
|
+
// This is for a Dialog in a Popover with its modal property set to true
|
|
15
|
+
|
|
16
|
+
// The element that serves as the dialog container has a role of dialog
|
|
17
|
+
let dialog = queryByText(container, "Which color do you prefer?");
|
|
18
|
+
assert(dialog, "Dialog not found");
|
|
19
|
+
assert(dialog.parentElement, "Dialog parent not found");
|
|
20
|
+
dialog = dialog.parentElement;
|
|
21
|
+
expect(dialog).toHaveAttribute("role", "dialog");
|
|
22
|
+
|
|
23
|
+
// All elements required to operate the dialog are descendants of the
|
|
24
|
+
// element that has role dialog
|
|
25
|
+
|
|
26
|
+
// The dialog container element has aria-modal set to true
|
|
27
|
+
expect(dialog).toHaveAttribute("aria-modal", "true");
|
|
28
|
+
|
|
29
|
+
// The dialog has either:
|
|
30
|
+
// * A value set for the aria-labelledby property that refers to a
|
|
31
|
+
// visible dialog title
|
|
32
|
+
// * A label specified by aria-label
|
|
33
|
+
const header = queryByText(container, "Color preference");
|
|
34
|
+
assert(header, "Header not found");
|
|
35
|
+
expect(dialog).toHaveAttribute("aria-labelledby", header.id);
|
|
36
|
+
|
|
37
|
+
// Optionally, the aria-describedby property is set on the element with
|
|
38
|
+
// the dialog role to indicate which element or elements in the dialog
|
|
39
|
+
// contain content that describes the primary purpose or message of the
|
|
40
|
+
// dialog. Specifying descriptive elements enables screen readers to
|
|
41
|
+
// announce the description along with the dialog title and initially
|
|
42
|
+
// focused element when the dialog opens, which is typically helpful
|
|
43
|
+
// only when the descriptive content is simple and can easily be
|
|
44
|
+
// understood without structural information. It is advisable to omit
|
|
45
|
+
// specifying aria-describedby if the dialog content includes semantic
|
|
46
|
+
// structures, such as lists, tables, or multiple paragraphs, that need
|
|
47
|
+
// to be perceived in order to easily understand the content, i.e., if
|
|
48
|
+
// the content would be difficult to understand when announced as a
|
|
49
|
+
// single unbroken string
|
|
50
|
+
expect(dialog).not.toHaveAttribute("aria-describedby");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Popover, PopoverContent } from "../../../src/Popover/index";
|
|
2
|
+
import { Dialog, DialogBody, DialogFooter, DialogButton } from "../../../src/Dialog/index";
|
|
3
|
+
|
|
4
|
+
export default function CustomDialog($props: any) {
|
|
5
|
+
@render {
|
|
6
|
+
<Popover visible callback={$props.callback}>
|
|
7
|
+
<PopoverContent>
|
|
8
|
+
<Dialog>
|
|
9
|
+
<DialogBody>
|
|
10
|
+
<p>Which color do you prefer?</p>
|
|
11
|
+
</DialogBody>
|
|
12
|
+
<DialogFooter>
|
|
13
|
+
<DialogButton value="blue">Blue</DialogButton>
|
|
14
|
+
<DialogButton value="red">Red</DialogButton>
|
|
15
|
+
</DialogFooter>
|
|
16
|
+
</Dialog>
|
|
17
|
+
</PopoverContent>
|
|
18
|
+
</Popover>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Popover, PopoverContent } from '../../../src/Popover/index';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogHeader,
|
|
5
|
+
DialogBody,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogButton
|
|
8
|
+
} from '../../../src/Dialog/index';
|
|
9
|
+
|
|
10
|
+
export default function DialogAccessibility($props: any) {
|
|
11
|
+
$props.modal ??= false;
|
|
12
|
+
$props.alert ??= false;
|
|
13
|
+
$props.ariaAnnounce ??= true;
|
|
14
|
+
|
|
15
|
+
let color = '';
|
|
16
|
+
|
|
17
|
+
@render {
|
|
18
|
+
<Popover visible modal={$props.modal}>
|
|
19
|
+
<PopoverContent>
|
|
20
|
+
<Dialog alert={$props.alert}>
|
|
21
|
+
<DialogHeader>Color preference</DialogHeader>
|
|
22
|
+
<DialogBody ariaAnnounce={$props.ariaAnnounce}>Which color do you prefer?</DialogBody>
|
|
23
|
+
<DialogFooter>
|
|
24
|
+
<DialogButton value="blue" onclick={() => (color = 'blue')}>Blue</DialogButton>
|
|
25
|
+
<DialogButton value="red" onclick={() => (color = 'red')}>Red</DialogButton>
|
|
26
|
+
</DialogFooter>
|
|
27
|
+
</Dialog>
|
|
28
|
+
</PopoverContent>
|
|
29
|
+
</Popover>
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { showPopoverPrompt } from "../../../src/Popover";
|
|
2
|
+
import CustomDialog from "./CustomDialog.torp";
|
|
3
|
+
|
|
4
|
+
export default function DialogFunction($props: any) {
|
|
5
|
+
let $state = $watch({ color: "" });
|
|
6
|
+
|
|
7
|
+
async function show() {
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
$state.color = await showPopoverPrompt(CustomDialog, null, $parent);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@render {
|
|
13
|
+
<div>
|
|
14
|
+
<button type="button" onclick={show}>Show the modal</button>
|
|
15
|
+
|
|
16
|
+
@if ($state.color) {
|
|
17
|
+
<p>Your favorite color is {$state.color}?!</p>
|
|
18
|
+
}
|
|
19
|
+
</div>
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Popover, PopoverContent } from "../../../src/Popover/index";
|
|
2
|
+
import { Dialog, DialogBody, DialogFooter, DialogButton } from "../../../src/Dialog/index";
|
|
3
|
+
|
|
4
|
+
export default function DialogVariable($props: any) {
|
|
5
|
+
let $state = $watch({
|
|
6
|
+
color: "",
|
|
7
|
+
showPopover: false
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
@render {
|
|
11
|
+
<div>
|
|
12
|
+
<button type="button" onclick={(e) => ($state.showPopover = !$state.showPopover)}>
|
|
13
|
+
Show the modal
|
|
14
|
+
</button>
|
|
15
|
+
|
|
16
|
+
@if ($state.color) {
|
|
17
|
+
<p>Your favorite color is {$state.color}?!</p>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
<Popover visible={$state.showPopover}>
|
|
21
|
+
<PopoverContent>
|
|
22
|
+
<Dialog>
|
|
23
|
+
<DialogBody>
|
|
24
|
+
<p>Which color do you prefer?</p>
|
|
25
|
+
</DialogBody>
|
|
26
|
+
<DialogFooter>
|
|
27
|
+
<DialogButton value="blue" onclick={() => ($state.color = "blue")}>Blue</DialogButton>
|
|
28
|
+
<DialogButton value="red" onclick={() => ($state.color = "red")}>Red</DialogButton>
|
|
29
|
+
</DialogFooter>
|
|
30
|
+
</Dialog>
|
|
31
|
+
</PopoverContent>
|
|
32
|
+
</Popover>
|
|
33
|
+
</div>
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getByText, queryByText } from "@testing-library/dom";
|
|
2
|
+
import "@testing-library/jest-dom/vitest";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { mount } from "@torpor/view";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import DialogFunction from "./components/DialogFunction.torp";
|
|
7
|
+
|
|
8
|
+
describe("Dialog", () => {
|
|
9
|
+
it("Showing with a function", async () => {
|
|
10
|
+
const container = document.createElement("div");
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
mount(container, DialogFunction);
|
|
13
|
+
|
|
14
|
+
// By default, the modal shouldn't be shown
|
|
15
|
+
expect(queryByText(container, "Which color do you prefer?")).not.toBeInTheDocument();
|
|
16
|
+
|
|
17
|
+
// Clicking the button should show the modal
|
|
18
|
+
await userEvent.click(getByText(container, "Show the modal"));
|
|
19
|
+
expect(queryByText(container, "Which color do you prefer?")).toBeInTheDocument();
|
|
20
|
+
|
|
21
|
+
// Clicking the blue button should hide the modal and set the value
|
|
22
|
+
await userEvent.click(getByText(container, "Blue"));
|
|
23
|
+
expect(queryByText(container, "Which color do you prefer?")).not.toBeInTheDocument();
|
|
24
|
+
expect(queryByText(container, "Your favorite color is blue?!")).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getByText, queryByText } from "@testing-library/dom";
|
|
2
|
+
import "@testing-library/jest-dom/vitest";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { mount } from "@torpor/view";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import DialogVariable from "./components/DialogVariable.torp";
|
|
7
|
+
|
|
8
|
+
describe("Dialog", () => {
|
|
9
|
+
it("Showing with a variable", async () => {
|
|
10
|
+
const container = document.createElement("div");
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
mount(container, DialogVariable);
|
|
13
|
+
|
|
14
|
+
// By default, the modal shouldn't be shown
|
|
15
|
+
expect(queryByText(container, "Which color do you prefer?")).not.toBeInTheDocument();
|
|
16
|
+
expect(queryByText(container, "Your favorite color is blue?!")).not.toBeInTheDocument();
|
|
17
|
+
|
|
18
|
+
// Clicking the button should show the modal
|
|
19
|
+
await userEvent.click(getByText(container, "Show the modal"));
|
|
20
|
+
expect(queryByText(container, "Which color do you prefer?")).toBeInTheDocument();
|
|
21
|
+
|
|
22
|
+
// Clicking the blue button should hide the modal and set the value
|
|
23
|
+
await userEvent.click(getByText(container, "Blue"));
|
|
24
|
+
expect(queryByText(container, "Which color do you prefer?")).not.toBeInTheDocument();
|
|
25
|
+
expect(queryByText(container, "Your favorite color is blue?!")).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getByText, queryByText } from "@testing-library/dom";
|
|
2
|
+
import "@testing-library/jest-dom/vitest";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { mount } from "@torpor/view";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import NavMenuAccessibility from "./components/NavMenuAccessibility.torp";
|
|
7
|
+
|
|
8
|
+
describe("NavMenu", () => {
|
|
9
|
+
it("Accessibility", async () => {
|
|
10
|
+
const container = document.createElement("div");
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
mount(container, NavMenuAccessibility);
|
|
13
|
+
|
|
14
|
+
// Tests from https://www.w3.org/WAI/ARIA/apg/patterns/menu/
|
|
15
|
+
// NOTE: NavMenu is a menubar
|
|
16
|
+
|
|
17
|
+
// A menu is a container of items that represent choices. The element serving as the menu has
|
|
18
|
+
// a role of either menu or menubar
|
|
19
|
+
expect(queryByText(container, "Home")?.parentElement).toHaveAttribute("role", "menubar");
|
|
20
|
+
|
|
21
|
+
// The items contained in a menu are child elements of the containing menu or menubar and have
|
|
22
|
+
// any of the following roles:
|
|
23
|
+
// * menuitem
|
|
24
|
+
// * menuitemcheckbox
|
|
25
|
+
// * menuitemradio
|
|
26
|
+
expect(queryByText(container, "Home")).toHaveAttribute("role", "menuitem");
|
|
27
|
+
expect(queryByText(container, "Page")).toHaveAttribute("role", "menuitem");
|
|
28
|
+
expect(queryByText(container, "Disabled")).toHaveAttribute("role", "menuitem");
|
|
29
|
+
expect(queryByText(container, "Subpages")).toHaveAttribute("role", "menuitem");
|
|
30
|
+
|
|
31
|
+
// If activating a menuitem opens a submenu, the menuitem is known as a parent menuitem. A submenu's
|
|
32
|
+
// menu element is:
|
|
33
|
+
// * Contained inside the same menu element as its parent menuitem
|
|
34
|
+
// * Is the sibling element immediately following its parent menuitem
|
|
35
|
+
|
|
36
|
+
// A parent menuitem has aria-haspopup set to either menu or true
|
|
37
|
+
expect(queryByText(container, "Subpages")).toHaveAttribute("aria-haspopup", "true");
|
|
38
|
+
|
|
39
|
+
// A parent menuitem has aria-expanded set to false when its child menu is not visible and set
|
|
40
|
+
// to true when the child menu is visible
|
|
41
|
+
expect(queryByText(container, "Subpages")).toHaveAttribute("aria-expanded", "false");
|
|
42
|
+
await userEvent.click(getByText(container, "Subpages"));
|
|
43
|
+
expect(queryByText(container, "Subpages")).toHaveAttribute("aria-expanded", "true");
|
|
44
|
+
|
|
45
|
+
// One of the following approaches is used to enable scripts to move focus among items in a
|
|
46
|
+
// menu as described in Keyboard Navigation Inside Components:
|
|
47
|
+
// * The menu container has tabindex set to -1 or 0 and aria-activedescendant set to the ID
|
|
48
|
+
// of the focused item
|
|
49
|
+
// * Each item in the menu has tabindex set to -1, except in a menubar, where the first item
|
|
50
|
+
// has tabindex set to 0
|
|
51
|
+
|
|
52
|
+
// When a menuitemcheckbox or menuitemradio is checked, aria-checked is set to true
|
|
53
|
+
// NOTE: We don't (currently?) have checkboxes or radios in NavMenus
|
|
54
|
+
|
|
55
|
+
// When a menu item is disabled, aria-disabled is set to true
|
|
56
|
+
expect(queryByText(container, "Page")).not.toHaveAttribute("aria-disabled");
|
|
57
|
+
expect(queryByText(container, "Disabled")).toHaveAttribute("aria-disabled", "true");
|
|
58
|
+
|
|
59
|
+
// Items in a menu may be divided into groups by placing an element with a role of separator
|
|
60
|
+
// between groups. For example, this technique should be used when a menu contains a set of
|
|
61
|
+
// menuitemradio items
|
|
62
|
+
expect(queryByText(container, "/")).toHaveAttribute("role", "separator");
|
|
63
|
+
|
|
64
|
+
// All separators should have aria-orientation consistent with the separator's orientation
|
|
65
|
+
// NOTE: What does this mean? None of the example separators have aria-orientation set
|
|
66
|
+
|
|
67
|
+
// If a menubar has a visible label, the element with role menubar has aria-labelledby set to
|
|
68
|
+
// a value that refers to the labelling element. Otherwise, the menubar element has a label
|
|
69
|
+
// provided by aria-label
|
|
70
|
+
// NOTE: We only support aria-label and it's required
|
|
71
|
+
expect(queryByText(container, "Home")?.parentElement).toHaveAttribute(
|
|
72
|
+
"aria-label",
|
|
73
|
+
"Site navigation",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// If a menubar is vertically oriented, it has aria-orientation set to vertical. The default
|
|
77
|
+
// value of aria-orientation for a menubar is horizontal
|
|
78
|
+
expect(queryByText(container, "Home")?.parentElement).toHaveAttribute(
|
|
79
|
+
"aria-orientation",
|
|
80
|
+
"horizontal",
|
|
81
|
+
);
|
|
82
|
+
expect(queryByText(container, "Subpage 1")?.parentElement).toHaveAttribute(
|
|
83
|
+
"aria-orientation",
|
|
84
|
+
"vertical",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Our own tests
|
|
88
|
+
|
|
89
|
+
// The group should be labeled
|
|
90
|
+
expect(queryByText(container, "Group header")).toBeInTheDocument();
|
|
91
|
+
const headerId = queryByText(container, "Group header")?.id;
|
|
92
|
+
expect(queryByText(container, "Group header")?.parentElement).toHaveAttribute(
|
|
93
|
+
"aria-labelledby",
|
|
94
|
+
headerId,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getByText, queryByText } from "@testing-library/dom";
|
|
2
|
+
import "@testing-library/jest-dom/vitest";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { mount } from "@torpor/view";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import NavMenu from "./components/NavMenu.torp";
|
|
7
|
+
|
|
8
|
+
describe("NavMenu", () => {
|
|
9
|
+
it("Basic operation", async () => {
|
|
10
|
+
const container = document.createElement("div");
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
mount(container, NavMenu);
|
|
13
|
+
|
|
14
|
+
// By default, body 1 should be displayed
|
|
15
|
+
expect(queryByText(container, "Home")).toBeInTheDocument();
|
|
16
|
+
expect(queryByText(container, "Subpage 1")).not.toBeInTheDocument();
|
|
17
|
+
|
|
18
|
+
// Clicking header 2 should hide body 1 and reveal body 2
|
|
19
|
+
await userEvent.click(getByText(container, "Subpages"));
|
|
20
|
+
expect(queryByText(container, "Subpage 1")).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NavMenu,
|
|
3
|
+
NavMenuLink,
|
|
4
|
+
NavMenuPopout,
|
|
5
|
+
NavMenuPopoutButton,
|
|
6
|
+
NavMenuPopoutContent,
|
|
7
|
+
NavMenuSeparator
|
|
8
|
+
} from "../../../src/NavMenu";
|
|
9
|
+
|
|
10
|
+
export default function NavMenu($props: any) {
|
|
11
|
+
@render {
|
|
12
|
+
<NavMenu ariaLabel="Site navigation">
|
|
13
|
+
<NavMenuLink href="/">Home</NavMenuLink>
|
|
14
|
+
<NavMenuLink href="/page1">Page 1</NavMenuLink>
|
|
15
|
+
<NavMenuSeparator />
|
|
16
|
+
<NavMenuPopout>
|
|
17
|
+
<NavMenuPopoutButton>Subpages</NavMenuPopoutButton>
|
|
18
|
+
<NavMenuPopoutContent>
|
|
19
|
+
<NavMenu ariaLabel="Sub site navigation" orientation="vertical">
|
|
20
|
+
<NavMenuLink href="/sub/page1">Subpage 1</NavMenuLink>
|
|
21
|
+
<NavMenuLink href="/sub/page2">Subpage 2</NavMenuLink>
|
|
22
|
+
<NavMenuLink href="/sub/page3">Subpage 3</NavMenuLink>
|
|
23
|
+
</NavMenu>
|
|
24
|
+
</NavMenuPopoutContent>
|
|
25
|
+
</NavMenuPopout>
|
|
26
|
+
</NavMenu>
|
|
27
|
+
}
|
|
28
|
+
}
|