@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +28 -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/Popover/Popover.torp +105 -0
  34. package/dist/Popover/PopoverContentAnchored.torp +107 -0
  35. package/dist/Popover/PopoverContentContextual.torp +99 -0
  36. package/dist/Popover/PopoverContentStatic.torp +78 -0
  37. package/dist/Popover/PopoverOverlay.torp +32 -0
  38. package/dist/Popover/PopoverTrigger.torp +29 -0
  39. package/dist/Popover/PopoverTriggerContextual.torp +37 -0
  40. package/dist/Popover/PopoverTriggerHover.torp +30 -0
  41. package/dist/Popover/PopoverTypes.ts +30 -0
  42. package/dist/Popover/index.cjs +222 -0
  43. package/dist/Popover/index.cjs.map +1 -0
  44. package/dist/Popover/index.js +183 -0
  45. package/dist/Popover/index.js.map +1 -0
  46. package/dist/Popover/mountClickPopover.ts +50 -0
  47. package/dist/Popover/mountContextPopover.ts +50 -0
  48. package/dist/Popover/mountHoverPopover.ts +57 -0
  49. package/dist/Popover/showPopover.ts +24 -0
  50. package/dist/Popover/showPopoverPrompt.ts +23 -0
  51. package/dist/TabGroup/TabContent.torp +4 -4
  52. package/dist/TabGroup/TabGroup.torp +58 -59
  53. package/dist/TabGroup/TabGroupTypes.ts +1 -0
  54. package/dist/TabGroup/TabHeader.torp +10 -8
  55. package/dist/TabGroup/TabList.torp +4 -3
  56. package/dist/TabGroup/TabTrigger.torp +6 -6
  57. package/dist/metafile-cjs.json +1 -1
  58. package/dist/metafile-esm.json +1 -1
  59. package/dist/mount/disableScroll.ts +28 -0
  60. package/dist/mount/hoverTrigger.ts +59 -0
  61. package/dist/mount/trapFocus.ts +58 -0
  62. package/dist/utils/documentEvents.ts +17 -0
  63. package/dist/utils/getScrollParent.ts +22 -0
  64. package/dist/utils/setContextPosition.ts +31 -0
  65. package/dist/utils/setPopoverPosition.ts +138 -0
  66. package/package.json +21 -6
  67. package/scripts/postbuild.ts +17 -5
  68. package/src/Accordion/Accordion.torp +53 -54
  69. package/src/Accordion/AccordionContent.torp +5 -5
  70. package/src/Accordion/AccordionHeader.torp +5 -4
  71. package/src/Accordion/AccordionItem.torp +13 -8
  72. package/src/Accordion/AccordionTrigger.torp +5 -5
  73. package/src/Accordion/AccordionTypes.ts +1 -0
  74. package/src/Dialog/Dialog.torp +156 -0
  75. package/src/Dialog/DialogBody.torp +34 -0
  76. package/src/Dialog/DialogButton.torp +48 -0
  77. package/src/Dialog/DialogFooter.torp +13 -0
  78. package/src/Dialog/DialogHeader.torp +34 -0
  79. package/src/Dialog/DialogTypes.ts +16 -0
  80. package/src/Dialog/index.ts +7 -0
  81. package/src/Dialog.ts +7 -0
  82. package/src/NavMenu/NavMenu.torp +178 -0
  83. package/src/NavMenu/NavMenuButton.torp +46 -0
  84. package/src/NavMenu/NavMenuElement.torp +43 -0
  85. package/src/NavMenu/NavMenuGroup.torp +30 -0
  86. package/src/NavMenu/NavMenuGroupHeader.torp +30 -0
  87. package/src/NavMenu/NavMenuLink.torp +52 -0
  88. package/src/NavMenu/NavMenuPopout.torp +69 -0
  89. package/src/NavMenu/NavMenuPopoutButton.torp +120 -0
  90. package/src/NavMenu/NavMenuPopoutContent.torp +79 -0
  91. package/src/NavMenu/NavMenuSeparator.torp +14 -0
  92. package/src/NavMenu/NavMenuTypes.ts +32 -0
  93. package/src/NavMenu/index.ts +23 -0
  94. package/src/NavMenu.ts +23 -0
  95. package/src/Popover/Popover.torp +105 -0
  96. package/src/Popover/PopoverContentAnchored.torp +107 -0
  97. package/src/Popover/PopoverContentContextual.torp +99 -0
  98. package/src/Popover/PopoverContentStatic.torp +78 -0
  99. package/src/Popover/PopoverOverlay.torp +32 -0
  100. package/src/Popover/PopoverTrigger.torp +29 -0
  101. package/src/Popover/PopoverTriggerContextual.torp +37 -0
  102. package/src/Popover/PopoverTriggerHover.torp +30 -0
  103. package/src/Popover/PopoverTypes.ts +30 -0
  104. package/src/Popover/index.ts +29 -0
  105. package/src/Popover/mountClickPopover.ts +50 -0
  106. package/src/Popover/mountContextPopover.ts +50 -0
  107. package/src/Popover/mountHoverPopover.ts +57 -0
  108. package/src/Popover/showPopover.ts +24 -0
  109. package/src/Popover/showPopoverPrompt.ts +23 -0
  110. package/src/Popover.ts +29 -0
  111. package/src/TabGroup/TabContent.torp +4 -4
  112. package/src/TabGroup/TabGroup.torp +58 -59
  113. package/src/TabGroup/TabGroupTypes.ts +1 -0
  114. package/src/TabGroup/TabHeader.torp +10 -8
  115. package/src/TabGroup/TabList.torp +4 -3
  116. package/src/TabGroup/TabTrigger.torp +6 -6
  117. package/src/mount/disableScroll.ts +28 -0
  118. package/src/mount/hoverTrigger.ts +59 -0
  119. package/src/mount/trapFocus.ts +58 -0
  120. package/src/utils/documentEvents.ts +17 -0
  121. package/src/utils/getScrollParent.ts +22 -0
  122. package/src/utils/setContextPosition.ts +31 -0
  123. package/src/utils/setPopoverPosition.ts +138 -0
  124. package/test/Accordion/accessibility.test.ts +0 -3
  125. package/test/Accordion/for-items.test.ts +2 -4
  126. package/test/Accordion/keyboard.test.ts +0 -3
  127. package/test/Accordion/multiple-item-toggle.test.ts +3 -5
  128. package/test/Accordion/single-item-toggle.test.ts +3 -5
  129. package/test/Dialog/accessibility-alert.test.ts +40 -0
  130. package/test/Dialog/accessibility-modal.test.ts +52 -0
  131. package/test/Dialog/components/CustomDialog.torp +20 -0
  132. package/test/Dialog/components/DialogAccessibility.torp +31 -0
  133. package/test/Dialog/components/DialogFunction.torp +22 -0
  134. package/test/Dialog/components/DialogVariable.torp +35 -0
  135. package/test/Dialog/function.test.ts +26 -0
  136. package/test/Dialog/variable.test.ts +27 -0
  137. package/test/NavMenu/accessibility.test.ts +97 -0
  138. package/test/NavMenu/basic.test.ts +22 -0
  139. package/test/NavMenu/components/NavMenu.torp +28 -0
  140. package/test/NavMenu/components/NavMenuAccessibility.torp +35 -0
  141. package/test/TabGroup/for-items.test.ts +2 -4
  142. package/test/TabGroup/item-trigger.test.ts +3 -5
  143. package/tsup.config.ts +8 -1
@@ -0,0 +1,59 @@
1
+ interface HoverTriggerOptions {
2
+ onHover: (node: HTMLElement, e: TouchEvent | MouseEvent) => void;
3
+ onExit?: (node: HTMLElement, e: TouchEvent | MouseEvent) => void;
4
+ hoverDelay?: number;
5
+ touchDelay?: number;
6
+ }
7
+
8
+ export default function hoverTrigger(node: HTMLElement, options: HoverTriggerOptions): () => void {
9
+ const { onHover, onExit } = options;
10
+ const hoverDelay = options.hoverDelay || 1000;
11
+ const touchDelay = options.touchDelay || 1000;
12
+
13
+ let interval: NodeJS.Timeout | undefined;
14
+
15
+ // Add click event handlers to the node
16
+ node.addEventListener("mouseenter", handleMouseEnter);
17
+ node.addEventListener("mouseleave", handleMouseLeave);
18
+ node.addEventListener("mousemove", handleMouseMove);
19
+ node.addEventListener("touchstart", handleMouseEnter);
20
+ node.addEventListener("touchend", handleMouseLeave);
21
+
22
+ function handleMouseEnter(e: MouseEvent | TouchEvent) {
23
+ // Handle the hover after a delay
24
+ e.preventDefault();
25
+ if (!interval) {
26
+ const delay = e.type.startsWith("mouse") ? hoverDelay : touchDelay;
27
+ interval = setTimeout(() => onHover(node, e), delay);
28
+ }
29
+ }
30
+
31
+ function handleMouseLeave(e: MouseEvent | TouchEvent) {
32
+ // Prevent short hovers or touches from firing the event
33
+ if (interval) {
34
+ clearTimeout(interval);
35
+ interval = undefined;
36
+ }
37
+
38
+ if (onExit) {
39
+ onExit(node, e);
40
+ }
41
+ }
42
+
43
+ function handleMouseMove(e: MouseEvent) {
44
+ // Reset the interval on mouse move
45
+ if (interval) {
46
+ clearTimeout(interval);
47
+ interval = setTimeout(() => onHover(node, e), hoverDelay);
48
+ }
49
+ }
50
+
51
+ // Remove the event listeners from the node on destroy
52
+ return () => {
53
+ node.removeEventListener("mouseenter", handleMouseEnter);
54
+ node.removeEventListener("mouseleave", handleMouseLeave);
55
+ node.removeEventListener("mousemove", handleMouseMove);
56
+ node.removeEventListener("touchstart", handleMouseEnter);
57
+ node.removeEventListener("touchend", handleMouseLeave);
58
+ };
59
+ }
@@ -0,0 +1,58 @@
1
+ export interface TrapFocusOptions {
2
+ focusableElements: HTMLElement[];
3
+ }
4
+
5
+ export function trapFocus(node: HTMLElement, options?: TrapFocusOptions): () => void {
6
+ options = options || { focusableElements: [] };
7
+
8
+ // Selectors for all things that are focusable:
9
+ const focusableSelectors = [
10
+ "a[href]",
11
+ "area[href]",
12
+ 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
13
+ "select:not([disabled]):not([aria-hidden])",
14
+ "textarea:not([disabled]):not([aria-hidden])",
15
+ "button:not([disabled]):not([aria-hidden])",
16
+ "iframe",
17
+ "object",
18
+ "embed",
19
+ "[contenteditable]",
20
+ '[tabindex]:not([tabindex^="-"])',
21
+ ];
22
+
23
+ // Set the focusable elements array in the options, which can be used in the caller e.g. to focus the first element
24
+ options.focusableElements = Array.from(node.querySelectorAll(focusableSelectors.join()));
25
+
26
+ // Store first and last focusable elements
27
+ const firstFocusableElement = options.focusableElements[0] as HTMLElement;
28
+ const lastFocusableElement = options.focusableElements[
29
+ options.focusableElements.length - 1
30
+ ] as HTMLElement;
31
+
32
+ // Add the keydown event listener to the node
33
+ node.addEventListener("keydown", handleKey);
34
+
35
+ // On Tab or Shift+Tab, move through the focusable elements, wrapping at either end
36
+ function handleKey(e: KeyboardEvent) {
37
+ if (e.key === "Tab") {
38
+ if (e.shiftKey) {
39
+ if (document.activeElement === firstFocusableElement) {
40
+ // Shift + Tab on the first element, so move to the last one
41
+ lastFocusableElement.focus();
42
+ e.preventDefault();
43
+ }
44
+ } else {
45
+ if (document.activeElement === lastFocusableElement) {
46
+ // Tab on the last element, so move to the first one
47
+ firstFocusableElement.focus();
48
+ e.preventDefault();
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ // Remove the keydown event listener from the node on destroy
55
+ return () => {
56
+ node.removeEventListener("keydown", handleKey);
57
+ };
58
+ }
@@ -0,0 +1,17 @@
1
+ export function addDocumentEvent<K extends keyof DocumentEventMap>(
2
+ type: K,
3
+ listener: (this: Document, ev: DocumentEventMap[K]) => any
4
+ ): void {
5
+ if (typeof document !== 'undefined') {
6
+ document.addEventListener(type, listener);
7
+ }
8
+ }
9
+
10
+ export function removeDocumentEvent<K extends keyof DocumentEventMap>(
11
+ type: K,
12
+ listener: (this: Document, ev: DocumentEventMap[K]) => any
13
+ ): void {
14
+ if (typeof document !== 'undefined') {
15
+ document.removeEventListener(type, listener);
16
+ }
17
+ }
@@ -0,0 +1,22 @@
1
+ // From https://stackoverflow.com/a/76535387
2
+ export default function getScrollParent(node: Element): Element {
3
+ let currentParent = node.parentElement;
4
+ while (currentParent) {
5
+ if (isScrollable(currentParent)) {
6
+ return currentParent;
7
+ }
8
+ currentParent = currentParent.parentElement;
9
+ }
10
+ return document.scrollingElement || document.documentElement;
11
+ }
12
+
13
+ function isScrollable(node: Element): boolean {
14
+ if (!(node instanceof HTMLElement || node instanceof SVGElement)) {
15
+ return false;
16
+ }
17
+ const style = getComputedStyle(node);
18
+ return ["overflow", "overflow-x", "overflow-y"].some((propertyName) => {
19
+ const value = style.getPropertyValue(propertyName);
20
+ return value === "auto" || value === "scroll";
21
+ });
22
+ }
@@ -0,0 +1,31 @@
1
+ interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+
6
+ export default function setContextPosition(
7
+ anchor: HTMLElement,
8
+ content: HTMLElement,
9
+ position: Point,
10
+ ): void {
11
+ var anchorRect = anchor.getBoundingClientRect();
12
+ var contentRect = content.getBoundingClientRect();
13
+
14
+ content.style.position = "fixed";
15
+
16
+ // Try to make sure the content is entirely on the screen horizontally
17
+ // Just move it left or right -- the pointer is outside the content vertically so moving it left
18
+ // or right won't make the content appear over the cursor
19
+ let left = anchorRect.left + position.x;
20
+ left = Math.min(left, window.innerWidth - contentRect.width);
21
+ left = Math.max(left, 0);
22
+ content.style.left = left + "px";
23
+
24
+ // Try to make sure the content is entirely on the screen vertically
25
+ // Show it above the position if it wouldn't fit
26
+ let top = anchorRect.top + position.y;
27
+ if (top + contentRect.height > window.innerHeight) {
28
+ top -= contentRect.height;
29
+ }
30
+ content.style.top = top + "px";
31
+ }
@@ -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 user.click(getByText(container, "Item 1"));
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 user.click(getByText(container, "Item 4"));
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 user.click(getByText(container, "Header 1"));
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 user.click(getByText(container, "Header 2"));
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 user.click(getByText(container, "Header 2"));
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 user.click(getByText(container, "Header 1"));
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 user.click(getByText(container, "Header 2"));
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 user.click(getByText(container, "Header 2"));
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, PopoverContentStatic } 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
+ <PopoverContentStatic>
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
+ </PopoverContentStatic>
18
+ </Popover>
19
+ }
20
+ }
@@ -0,0 +1,31 @@
1
+ import { Popover, PopoverContentStatic } 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
+ <PopoverContentStatic>
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
+ </PopoverContentStatic>
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, PopoverContentStatic } 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
+ <PopoverContentStatic>
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
+ </PopoverContentStatic>
32
+ </Popover>
33
+ </div>
34
+ }
35
+ }