@torpor/ui 0.1.7 → 0.2.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 (276) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/Accordion/Accordion.torp +2 -2
  3. package/dist/Accordion/AccordionContent.torp +2 -2
  4. package/dist/Accordion/AccordionHeader.torp +2 -2
  5. package/dist/Accordion/AccordionItem.torp +2 -2
  6. package/dist/Accordion/AccordionTrigger.torp +2 -2
  7. package/dist/Accordion/index.d.ts +92 -0
  8. package/dist/Breadcrumb/Breadcrumb.torp +33 -0
  9. package/dist/Breadcrumb/BreadcrumbItem.torp +19 -0
  10. package/dist/Breadcrumb/BreadcrumbSeparator.torp +16 -0
  11. package/dist/Breadcrumb/index.d.ts +56 -0
  12. package/dist/Breadcrumb/index.js +14 -0
  13. package/dist/Calendar/Calendar.torp +265 -0
  14. package/dist/Calendar/CalendarDay.torp +67 -0
  15. package/dist/Calendar/CalendarTypes.ts +22 -0
  16. package/dist/Calendar/index.d.ts +38 -0
  17. package/dist/Calendar/index.js +10 -0
  18. package/dist/Calendar/index.js.map +1 -0
  19. package/dist/Dialog/Dialog.torp +3 -3
  20. package/dist/Dialog/DialogBody.torp +2 -2
  21. package/dist/Dialog/DialogButton.torp +2 -2
  22. package/dist/Dialog/DialogFooter.torp +2 -2
  23. package/dist/Dialog/DialogHeader.torp +2 -2
  24. package/dist/Dialog/index.d.ts +92 -0
  25. package/dist/Disclosure/Disclosure.torp +69 -0
  26. package/dist/Disclosure/DisclosureContent.torp +36 -0
  27. package/dist/Disclosure/DisclosureHeader.torp +34 -0
  28. package/dist/Disclosure/DisclosureTrigger.torp +36 -0
  29. package/dist/Disclosure/DisclosureTypes.ts +14 -0
  30. package/dist/Disclosure/index.d.ts +74 -0
  31. package/dist/Disclosure/index.js +18 -0
  32. package/dist/Disclosure/index.js.map +1 -0
  33. package/{src/Offscreen/Offscreen.torp → dist/Drawer/Drawer.torp} +10 -19
  34. package/dist/Drawer/index.d.ts +20 -0
  35. package/dist/Drawer/index.js +6 -0
  36. package/dist/Drawer/index.js.map +1 -0
  37. package/dist/Menu/Menu.torp +160 -0
  38. package/dist/Menu/MenuButton.torp +53 -0
  39. package/dist/Menu/MenuCheck.torp +70 -0
  40. package/dist/Menu/MenuGroup.torp +19 -0
  41. package/dist/Menu/MenuIndicator.torp +29 -0
  42. package/dist/Menu/MenuPopout.torp +71 -0
  43. package/dist/Menu/MenuPopoutContent.torp +74 -0
  44. package/dist/Menu/MenuPopoutTrigger.torp +117 -0
  45. package/dist/Menu/MenuRadio.torp +88 -0
  46. package/dist/Menu/MenuRadioGroup.torp +86 -0
  47. package/dist/Menu/MenuSeparator.torp +14 -0
  48. package/dist/Menu/MenuTypes.ts +58 -0
  49. package/dist/Menu/index.d.ts +200 -0
  50. package/dist/Menu/index.js +46 -0
  51. package/dist/Menu/index.js.map +1 -0
  52. package/dist/MenuBar/MenuBar.torp +150 -0
  53. package/dist/MenuBar/MenuBarItem.torp +104 -0
  54. package/dist/MenuBar/MenuBarItemContent.torp +108 -0
  55. package/dist/MenuBar/MenuBarItemTrigger.torp +89 -0
  56. package/dist/MenuBar/MenuBarTypes.ts +36 -0
  57. package/dist/MenuBar/index.d.ts +74 -0
  58. package/dist/MenuBar/index.js +18 -0
  59. package/dist/MenuBar/index.js.map +1 -0
  60. package/dist/NavMenu/NavMenu.torp +2 -2
  61. package/dist/NavMenu/NavMenuButton.torp +2 -2
  62. package/dist/NavMenu/NavMenuElement.torp +2 -2
  63. package/dist/NavMenu/NavMenuGroup.torp +2 -2
  64. package/dist/NavMenu/NavMenuGroupHeader.torp +2 -2
  65. package/dist/NavMenu/NavMenuLink.torp +2 -2
  66. package/dist/NavMenu/NavMenuPopout.torp +2 -2
  67. package/dist/NavMenu/NavMenuPopoutButton.torp +3 -3
  68. package/dist/NavMenu/NavMenuPopoutContent.torp +29 -27
  69. package/dist/NavMenu/NavMenuSeparator.torp +2 -2
  70. package/dist/NavMenu/index.d.ts +182 -0
  71. package/dist/Notification/Notification.torp +48 -0
  72. package/dist/Notification/NotificationClose.torp +28 -0
  73. package/dist/Notification/NotificationContainer.torp +14 -0
  74. package/dist/Notification/NotificationContent.torp +13 -0
  75. package/dist/Notification/NotificationTypes.ts +5 -0
  76. package/dist/Notification/index.d.ts +74 -0
  77. package/dist/Notification/index.js +36 -0
  78. package/dist/Notification/index.js.map +1 -0
  79. package/dist/Notification/showNotification.ts +30 -0
  80. package/dist/Popover/Popover.torp +2 -2
  81. package/dist/Popover/PopoverContent.torp +31 -28
  82. package/dist/Popover/PopoverContentAnchored.torp +39 -38
  83. package/dist/Popover/PopoverContentContextual.torp +39 -36
  84. package/dist/Popover/PopoverOverlay.torp +3 -3
  85. package/dist/Popover/PopoverTrigger.torp +2 -2
  86. package/dist/Popover/PopoverTriggerContextual.torp +2 -2
  87. package/dist/Popover/PopoverTriggerHover.torp +3 -3
  88. package/dist/Popover/index.d.ts +146 -0
  89. package/dist/TabGroup/TabContent.torp +2 -2
  90. package/dist/TabGroup/TabGroup.torp +2 -2
  91. package/dist/TabGroup/TabHeader.torp +2 -2
  92. package/dist/TabGroup/TabList.torp +2 -2
  93. package/dist/TabGroup/TabTrigger.torp +2 -2
  94. package/dist/TabGroup/index.d.ts +92 -0
  95. package/dist/icons/ArrowDown.torp +15 -0
  96. package/dist/icons/ArrowLeft.torp +15 -0
  97. package/dist/icons/ArrowRight.torp +15 -0
  98. package/dist/icons/ArrowUp.torp +15 -0
  99. package/dist/icons/Bars.torp +17 -0
  100. package/dist/icons/Check.torp +15 -0
  101. package/dist/icons/ChevronDown.torp +15 -0
  102. package/dist/icons/ChevronLeft.torp +15 -0
  103. package/dist/icons/ChevronRight.torp +15 -0
  104. package/dist/icons/ChevronUp.torp +15 -0
  105. package/dist/icons/Cross.torp +16 -0
  106. package/dist/icons/Loading.torp +16 -0
  107. package/dist/icons/Minus.torp +15 -0
  108. package/dist/icons/Plus.torp +16 -0
  109. package/dist/icons/index.d.ts +254 -0
  110. package/dist/icons/index.js +58 -0
  111. package/dist/icons/index.js.map +1 -0
  112. package/dist/{motion.d.cts → index.d.ts} +2 -2
  113. package/dist/metafile-esm.json +1 -1
  114. package/dist/motion/fade.ts +7 -0
  115. package/dist/motion/index.d.ts +2 -0
  116. package/dist/{motion.js → motion/index.js} +1 -1
  117. package/dist/motion/index.js.map +1 -0
  118. package/dist/mount/index.d.ts +2 -0
  119. package/dist/utils/dateUtils.ts +244 -0
  120. package/dist/utils/index.d.ts +2 -0
  121. package/dist/utils/setPopoverPosition.ts +4 -0
  122. package/package.json +50 -34
  123. package/scripts/postbuild.ts +42 -5
  124. package/src/Accordion/Accordion.torp +2 -2
  125. package/src/Accordion/AccordionContent.torp +2 -2
  126. package/src/Accordion/AccordionHeader.torp +2 -2
  127. package/src/Accordion/AccordionItem.torp +2 -2
  128. package/src/Accordion/AccordionTrigger.torp +2 -2
  129. package/src/Breadcrumb/Breadcrumb.torp +33 -0
  130. package/src/Breadcrumb/BreadcrumbItem.torp +19 -0
  131. package/src/Breadcrumb/BreadcrumbSeparator.torp +16 -0
  132. package/src/Breadcrumb/index.ts +5 -0
  133. package/src/Calendar/Calendar.torp +265 -0
  134. package/src/Calendar/CalendarDay.torp +67 -0
  135. package/src/Calendar/CalendarTypes.ts +22 -0
  136. package/src/Calendar/index.ts +4 -0
  137. package/src/Dialog/Dialog.torp +3 -3
  138. package/src/Dialog/DialogBody.torp +2 -2
  139. package/src/Dialog/DialogButton.torp +2 -2
  140. package/src/Dialog/DialogFooter.torp +2 -2
  141. package/src/Dialog/DialogHeader.torp +2 -2
  142. package/src/Disclosure/Disclosure.torp +69 -0
  143. package/src/Disclosure/DisclosureContent.torp +36 -0
  144. package/src/Disclosure/DisclosureHeader.torp +34 -0
  145. package/src/Disclosure/DisclosureTrigger.torp +36 -0
  146. package/src/Disclosure/DisclosureTypes.ts +14 -0
  147. package/src/Disclosure/index.ts +6 -0
  148. package/{dist/Offscreen/Offscreen.torp → src/Drawer/Drawer.torp} +10 -19
  149. package/src/Drawer/index.ts +3 -0
  150. package/src/Menu/Menu.torp +160 -0
  151. package/src/Menu/MenuButton.torp +53 -0
  152. package/src/Menu/MenuCheck.torp +70 -0
  153. package/src/Menu/MenuGroup.torp +19 -0
  154. package/src/Menu/MenuIndicator.torp +29 -0
  155. package/src/Menu/MenuPopout.torp +71 -0
  156. package/src/Menu/MenuPopoutContent.torp +74 -0
  157. package/src/Menu/MenuPopoutTrigger.torp +117 -0
  158. package/src/Menu/MenuRadio.torp +88 -0
  159. package/src/Menu/MenuRadioGroup.torp +86 -0
  160. package/src/Menu/MenuSeparator.torp +14 -0
  161. package/src/Menu/MenuTypes.ts +58 -0
  162. package/src/Menu/index.ts +25 -0
  163. package/src/MenuBar/MenuBar.torp +150 -0
  164. package/src/MenuBar/MenuBarItem.torp +104 -0
  165. package/src/MenuBar/MenuBarItemContent.torp +108 -0
  166. package/src/MenuBar/MenuBarItemTrigger.torp +89 -0
  167. package/src/MenuBar/MenuBarTypes.ts +36 -0
  168. package/src/MenuBar/index.ts +6 -0
  169. package/src/NavMenu/NavMenu.torp +2 -2
  170. package/src/NavMenu/NavMenuButton.torp +2 -2
  171. package/src/NavMenu/NavMenuElement.torp +2 -2
  172. package/src/NavMenu/NavMenuGroup.torp +2 -2
  173. package/src/NavMenu/NavMenuGroupHeader.torp +2 -2
  174. package/src/NavMenu/NavMenuLink.torp +2 -2
  175. package/src/NavMenu/NavMenuPopout.torp +2 -2
  176. package/src/NavMenu/NavMenuPopoutButton.torp +3 -3
  177. package/src/NavMenu/NavMenuPopoutContent.torp +29 -27
  178. package/src/NavMenu/NavMenuSeparator.torp +2 -2
  179. package/src/Notification/Notification.torp +48 -0
  180. package/src/Notification/NotificationClose.torp +28 -0
  181. package/src/Notification/NotificationContainer.torp +14 -0
  182. package/src/Notification/NotificationContent.torp +13 -0
  183. package/src/Notification/NotificationTypes.ts +5 -0
  184. package/src/Notification/index.ts +13 -0
  185. package/src/Notification/showNotification.ts +30 -0
  186. package/src/Popover/Popover.torp +2 -2
  187. package/src/Popover/PopoverContent.torp +31 -28
  188. package/src/Popover/PopoverContentAnchored.torp +39 -38
  189. package/src/Popover/PopoverContentContextual.torp +39 -36
  190. package/src/Popover/PopoverOverlay.torp +3 -3
  191. package/src/Popover/PopoverTrigger.torp +2 -2
  192. package/src/Popover/PopoverTriggerContextual.torp +2 -2
  193. package/src/Popover/PopoverTriggerHover.torp +3 -3
  194. package/src/Slider/Slider.torp +83 -0
  195. package/src/Slider/index.ts +3 -0
  196. package/src/TabGroup/TabContent.torp +2 -2
  197. package/src/TabGroup/TabGroup.torp +2 -2
  198. package/src/TabGroup/TabHeader.torp +2 -2
  199. package/src/TabGroup/TabList.torp +2 -2
  200. package/src/TabGroup/TabTrigger.torp +2 -2
  201. package/src/icons/ArrowDown.torp +15 -0
  202. package/src/icons/ArrowLeft.torp +15 -0
  203. package/src/icons/ArrowRight.torp +15 -0
  204. package/src/icons/ArrowUp.torp +15 -0
  205. package/src/icons/Bars.torp +17 -0
  206. package/src/icons/Check.torp +15 -0
  207. package/src/icons/ChevronDown.torp +15 -0
  208. package/src/icons/ChevronLeft.torp +15 -0
  209. package/src/icons/ChevronRight.torp +15 -0
  210. package/src/icons/ChevronUp.torp +15 -0
  211. package/src/icons/Cross.torp +16 -0
  212. package/src/icons/Loading.torp +16 -0
  213. package/src/icons/Minus.torp +15 -0
  214. package/src/icons/Plus.torp +16 -0
  215. package/src/icons/index.ts +31 -0
  216. package/src/motion/fade.ts +2 -2
  217. package/src/motion/index.ts +3 -0
  218. package/src/utils/dateUtils.ts +244 -0
  219. package/src/utils/setPopoverPosition.ts +4 -0
  220. package/test/Breadcrumb/accessibility.test.ts +29 -0
  221. package/test/Breadcrumb/basic.test.ts +17 -0
  222. package/test/Breadcrumb/components/BreadcrumbTest.torp +15 -0
  223. package/test/Calendar/components/CalendarTest.torp +9 -0
  224. package/test/Calendar/default.test.ts +18 -0
  225. package/test/Dialog/components/DialogAccessibility.torp +5 -5
  226. package/test/Disclosure/accessibility.test.ts +30 -0
  227. package/test/Disclosure/components/DisclosureTest.torp +17 -0
  228. package/test/Disclosure/default.test.ts +15 -0
  229. package/test/Disclosure/toggle.test.ts +19 -0
  230. package/test/Menu/accessibility.test.ts +82 -0
  231. package/test/Menu/basic.test.ts +57 -0
  232. package/test/Menu/components/MenuAccessibility.torp +59 -0
  233. package/test/Menu/components/MenuBasic.torp +58 -0
  234. package/test/MenuBar/accessibility.test.ts +86 -0
  235. package/test/MenuBar/basic.test.ts +21 -0
  236. package/test/MenuBar/components/MenuBarAccessibility.torp +34 -0
  237. package/test/MenuBar/components/MenuBarBasic.torp +25 -0
  238. package/test/Notification/accessibility.test.ts +21 -0
  239. package/test/Notification/components/CustomNotification.torp +14 -0
  240. package/test/Notification/components/NotificationAccessibility.torp +14 -0
  241. package/test/Notification/components/NotificationFunction.torp +20 -0
  242. package/test/Notification/components/NotificationVariable.torp +22 -0
  243. package/test/Notification/function.test.ts +25 -0
  244. package/test/Notification/variable.test.ts +25 -0
  245. package/test/Popover/components/CustomDialog.torp +25 -0
  246. package/test/Popover/components/PromptFunction.torp +19 -0
  247. package/test/Popover/components/PromptVariable.torp +38 -0
  248. package/test/Popover/function.test.ts +26 -0
  249. package/test/Popover/variable.test.ts +27 -0
  250. package/tsup.config.ts +12 -4
  251. package/dist/Accordion/index.cjs +0 -53
  252. package/dist/Accordion/index.cjs.map +0 -1
  253. package/dist/Dialog/index.cjs +0 -53
  254. package/dist/Dialog/index.cjs.map +0 -1
  255. package/dist/NavMenu/index.cjs +0 -78
  256. package/dist/NavMenu/index.cjs.map +0 -1
  257. package/dist/Offscreen/index.cjs +0 -33
  258. package/dist/Offscreen/index.cjs.map +0 -1
  259. package/dist/Offscreen/index.js +0 -6
  260. package/dist/Popover/index.cjs +0 -222
  261. package/dist/Popover/index.cjs.map +0 -1
  262. package/dist/TabGroup/index.cjs +0 -53
  263. package/dist/TabGroup/index.cjs.map +0 -1
  264. package/dist/metafile-cjs.json +0 -1
  265. package/dist/motion.cjs +0 -34
  266. package/dist/motion.cjs.map +0 -1
  267. package/dist/motion.d.ts +0 -6
  268. package/dist/motion.js.map +0 -1
  269. package/src/Accordion.ts +0 -7
  270. package/src/Dialog.ts +0 -7
  271. package/src/NavMenu.ts +0 -23
  272. package/src/Offscreen/index.ts +0 -3
  273. package/src/Popover.ts +0 -29
  274. package/src/TabGroup.ts +0 -7
  275. package/src/motion.ts +0 -3
  276. /package/dist/{Offscreen → Breadcrumb}/index.js.map +0 -0
@@ -0,0 +1,160 @@
1
+ import { PopoverContextName, type PopoverContext } from "../Popover/PopoverTypes";
2
+ import {
3
+ MenuContextName,
4
+ type MenuContext,
5
+ type ItemState,
6
+ MenuPopoutContextName,
7
+ type MenuPopoutContext
8
+ } from "./MenuTypes";
9
+
10
+ /**
11
+ * A list of choices for the user, such as a set of actions or functions. A
12
+ * menu is usually hidden until made visible by the user.
13
+ *
14
+ * See [the WAI ARIA guidelines for Menus](https://www.w3.org/WAI/ARIA/apg/patterns/menu/).
15
+ *
16
+ * The structure of a Menu is:
17
+ *
18
+ * ```
19
+ * <Menu>
20
+ * <MenuButton />
21
+ * <MenuSeparator /> (optional)
22
+ * <MenuGroup> (optional)
23
+ * <MenuButton />
24
+ * </MenuGroup>
25
+ * </Menu>
26
+ * ```
27
+ *
28
+ * @prop {string} ariaLabel -- An ARIA label for describing the menu to screen readers
29
+ */
30
+ export default function Menu() {
31
+ // A collection containing the state and some functions for each item
32
+ let itemStates: ItemState[] = [];
33
+ let focusIndex = -1;
34
+
35
+ // Get stuff out of the PopoverContext
36
+ const popoverContext = $context[PopoverContextName] as PopoverContext;
37
+ if (!popoverContext) {
38
+ throw new Error("Menu must be contained within a Contextual, Prompt or Popover");
39
+ }
40
+ const { setVisible, handleButton, modal, visible } = popoverContext;
41
+
42
+ // Check whether this is a menu in a popout
43
+ const popoutContext = $context[MenuPopoutContextName] as MenuPopoutContext;
44
+ const inPopout = !!popoutContext;
45
+ const setPopoutVisible = popoutContext?.setVisible;
46
+
47
+ // Set the focusFirstElement method in the PopoverContext(s) so that it can be called after everything has been shown
48
+ popoverContext.focusFirstElement = focusFirstElement;
49
+ if (popoutContext) {
50
+ popoutContext.focusFirstElement = focusFirstElement;
51
+ }
52
+
53
+ // Set the context to pass down to items
54
+ let context: MenuContext = {
55
+ setVisible,
56
+ handleButton,
57
+ handleItemFocus,
58
+ handleItemKey,
59
+ visible,
60
+ registerItem
61
+ };
62
+ $context[MenuContextName] = context;
63
+
64
+ function focusFirstElement() {
65
+ if (itemStates.length) {
66
+ itemStates[0].setFocused();
67
+ }
68
+ }
69
+
70
+ function registerItem(setFocused: () => void) {
71
+ // Add the item"s state to the array and return its index
72
+ const newItemState: ItemState = {
73
+ setFocused
74
+ };
75
+ itemStates.push(newItemState);
76
+ itemStates = itemStates;
77
+ return { index: itemStates.length - 1 };
78
+ }
79
+
80
+ function handleItemFocus(index: number) {
81
+ focusIndex = index;
82
+ }
83
+
84
+ function handleItemKey(e: KeyboardEvent) {
85
+ switch (e.key) {
86
+ case "Esc":
87
+ case "Escape": {
88
+ e.preventDefault();
89
+ setVisible(false);
90
+ break;
91
+ }
92
+ case "Down":
93
+ case "ArrowDown": {
94
+ e.preventDefault();
95
+ focusItem("next");
96
+ break;
97
+ }
98
+ case "Up":
99
+ case "ArrowUp": {
100
+ e.preventDefault();
101
+ focusItem("previous");
102
+ break;
103
+ }
104
+ case "Left":
105
+ case "ArrowLeft": {
106
+ // TODO: This should be based on what side the menu shows on
107
+ if (inPopout) {
108
+ e.preventDefault();
109
+ setPopoutVisible(false);
110
+ }
111
+ break;
112
+ }
113
+ case "Home": {
114
+ e.preventDefault();
115
+ focusItem("start");
116
+ break;
117
+ }
118
+ case "End": {
119
+ e.preventDefault();
120
+ focusItem("end");
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ function focusItem(target: "start" | "previous" | "next" | "end") {
127
+ switch (target) {
128
+ case "start": {
129
+ focusIndex = 0;
130
+ break;
131
+ }
132
+ case "previous": {
133
+ focusIndex = Math.max(0, focusIndex - 1);
134
+ break;
135
+ }
136
+ case "next": {
137
+ focusIndex = Math.min(itemStates.length - 1, focusIndex + 1);
138
+ break;
139
+ }
140
+ case "end": {
141
+ focusIndex = itemStates.length - 1;
142
+ break;
143
+ }
144
+ }
145
+ itemStates[focusIndex]?.setFocused();
146
+ }
147
+
148
+ @render {
149
+ <div
150
+ {$props.id}
151
+ class={`torp-menu ${$props.class ?? ""}`.trim()}
152
+ tabindex="-1"
153
+ role="menu"
154
+ aria-label={$props.ariaLabel}
155
+ aria-orientation="vertical"
156
+ >
157
+ <slot />
158
+ </div>
159
+ }
160
+ }
@@ -0,0 +1,53 @@
1
+ import { MenuContextName, type MenuContext } from "./MenuTypes";
2
+
3
+ /**
4
+ * A button that is placed in a Menu or MenuGroup and signals a response by a user.
5
+ *
6
+ * @prop {any} value -- The value that should be returned when this button is pressed and the Dialog was invoked by calling showContextual, showPrompt or showPopover
7
+ * @prop {boolean} disabled -- Whether this button is disabled
8
+ */
9
+ export default function MenuButton() {
10
+ let button: HTMLButtonElement;
11
+
12
+ // Get stuff out of the MenuContext
13
+ const menuContext = $context[MenuContextName] as MenuContext;
14
+ if (!menuContext) {
15
+ throw new Error("MenuButton must be contained within a Menu");
16
+ }
17
+ const { registerItem, handleButton, handleItemKey, handleItemFocus } = menuContext;
18
+
19
+ // Register this item with the parent Menu and store the index
20
+ const { index } = registerItem(setFocused);
21
+
22
+ function handleKeyDown(e) {
23
+ handleItemKey(e);
24
+ if ($props.onkeydown) $props.onkeydown(e);
25
+ }
26
+
27
+ function handleClick(e) {
28
+ handleButton($props.value);
29
+ if ($props.onclick) $props.onclick(e);
30
+ }
31
+
32
+ function setFocused() {
33
+ button.focus();
34
+ }
35
+
36
+ @render {
37
+ <button
38
+ {$props.id}
39
+ class={`torp-menu-button ${$props.class ?? ""}`.trim()}
40
+ type="button"
41
+ {$props.disabled}
42
+ role="menuitem"
43
+ aria-disabled={$props.disabled ? "true" : undefined}
44
+ data-disabled={$props.disabled ? "true" : undefined}
45
+ onkeydown={handleKeyDown}
46
+ onclick={handleClick}
47
+ onfocus={() => handleItemFocus(index)}
48
+ &ref={button}
49
+ >
50
+ <slot />
51
+ </button>
52
+ }
53
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ MenuButtonContextName,
3
+ MenuContextName,
4
+ type MenuButtonContext,
5
+ type MenuContext
6
+ } from "./MenuTypes";
7
+
8
+ /**
9
+ * A button that is placed in a Menu or MenuGroup to allow the user to turn an
10
+ * option on or off.
11
+ *
12
+ * @prop {boolean} checked -- Whether this item is checked
13
+ * @prop {boolean} disabled -- Whether this item is disabled
14
+ */
15
+ export default function MenuCheck() {
16
+ let $state = $watch({
17
+ checked: $props.checked
18
+ });
19
+
20
+ $run(() => {
21
+ $props.checked = $state.checked;
22
+ });
23
+
24
+ let button: HTMLButtonElement;
25
+
26
+ // Get stuff out of the MenuContext
27
+ const menuContext = $context[MenuContextName] as MenuContext;
28
+ if (!menuContext) {
29
+ throw new Error("MenuCheck must be contained within a Menu");
30
+ }
31
+ const { registerItem, handleItemKey, handleItemFocus } = menuContext;
32
+
33
+ // Register this item with the parent Menu
34
+ const { index } = registerItem(setFocused);
35
+
36
+ // Set the context to pass down to items
37
+ let context: MenuButtonContext = {
38
+ state: $state
39
+ };
40
+ $context[MenuButtonContextName] = context;
41
+
42
+ function setFocused() {
43
+ button.focus();
44
+ }
45
+
46
+ function handleClick() {
47
+ $state.checked = !$state.checked;
48
+ if ($props.onclick) $props.onclick();
49
+ }
50
+
51
+ @render {
52
+ <button
53
+ {$props.id}
54
+ class={`torp-menu-check ${$props.class ?? ""}`.trim()}
55
+ type="button"
56
+ {$props.disabled}
57
+ role="menuitemcheckbox"
58
+ aria-checked={$props.checked ? "true" : "false"}
59
+ aria-disabled={$props.disabled ? "true" : undefined}
60
+ data-state={$props.checked ? "checked" : "unchecked"}
61
+ data-disabled={$props.disabled ? "true" : undefined}
62
+ onkeydown={handleItemKey}
63
+ onclick={handleClick}
64
+ onfocus={() => handleItemFocus(index)}
65
+ &ref={button}
66
+ >
67
+ <slot />
68
+ </button>
69
+ }
70
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A container for a logical group of menu items.
3
+ *
4
+ * @prop {string} ariaLabel -- An ARIA label for describing the group to screen readers
5
+ */
6
+ export default function MenuGroup() {
7
+ // TODO: Intercept the context to steal toggleItem??
8
+
9
+ @render {
10
+ <div
11
+ {$props.id}
12
+ class={`torp-menu-group ${$props.class ?? ""}`.trim()}
13
+ role="group"
14
+ aria-label={$props.ariaLabel}
15
+ >
16
+ <slot />
17
+ </div>
18
+ }
19
+ }
@@ -0,0 +1,29 @@
1
+ import { MenuButtonContextName, type MenuButtonContext } from "./MenuTypes";
2
+
3
+ /**
4
+ * An indicator for a MenuCheck or MenuRadio component, which is displayed
5
+ * when the value of checked is true.
6
+ *
7
+ * @prop {boolean} disabled -- Whether this item is disabled
8
+ */
9
+ export default function MenuIndicator() {
10
+ // Get stuff out of the MenuButtonContext
11
+ const context = $context[MenuButtonContextName] as MenuButtonContext;
12
+ if (!context) {
13
+ throw new Error("MenuIndicator must be contained within a MenuCheck or MenuRadio");
14
+ }
15
+ let $state = context.state;
16
+
17
+ @render {
18
+ @if ($state.checked) {
19
+ <div
20
+ {$props.id}
21
+ class={`torp-menu-indicator ${$props.class ?? ""}`.trim()}
22
+ aria-disabled={$props.disabled ? "true" : undefined}
23
+ data-disabled={$props.disabled ? "true" : undefined}
24
+ >
25
+ <slot />
26
+ </div>
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,71 @@
1
+ import { MenuPopoutContextName, type MenuPopoutContext } from "./MenuTypes";
2
+
3
+ /**
4
+ * A popout that is attached to a menu button and can be used to show a submenu.
5
+ *
6
+ * The structure of a MenuPopout is:
7
+ *
8
+ * ```
9
+ * <MenuPopout>
10
+ * <MenuPopoutTrigger /> (optional)
11
+ * <MenuPopoutContent />
12
+ * </MenuPopout>
13
+ * ```
14
+ *
15
+ * @prop {boolean} visible -- Whether the popout is visible
16
+ * @prop {(boolean) => void} ontoggle -- Called when the popout is opened or closed
17
+ * @prop {() => void} onopen -- Called when the popout has opened
18
+ * @prop {() => void} onclose -- Called when the popout has closed
19
+ */
20
+ export default function MenuPopout($props) {
21
+ $props.visible ??= false;
22
+
23
+ let $state = $watch({
24
+ visible: $props.visible
25
+ });
26
+
27
+ let result: any;
28
+
29
+ // Set the context to pass down to items
30
+ let context: MenuPopoutContext = {
31
+ setVisible,
32
+ state: $state
33
+ };
34
+ $context[MenuPopoutContextName] = context;
35
+
36
+ function setVisible(value: boolean) {
37
+ $state.visible = value;
38
+ }
39
+
40
+ let firstToggle = true;
41
+ $run(() => {
42
+ const ontoggle = $props.ontoggle;
43
+ const onopen = $props.onopen;
44
+ const onclose = $props.onclose;
45
+ const visible = $state.visible;
46
+ if (firstToggle) {
47
+ firstToggle = false;
48
+ } else {
49
+ if (ontoggle) ontoggle(visible);
50
+ if (visible) {
51
+ if (onopen) onopen()
52
+ } else {
53
+ if (onclose) onclose(result);
54
+ }
55
+ }
56
+ });
57
+
58
+ function handleMouseLeave() {
59
+ setVisible(false);
60
+ }
61
+
62
+ @render {
63
+ <div
64
+ {$props.id}
65
+ class={`torp-menu-popout ${$props.class ?? ""}`.trim()}
66
+ onmouseleave={handleMouseLeave}
67
+ >
68
+ <slot />
69
+ </div>
70
+ }
71
+ }
@@ -0,0 +1,74 @@
1
+
2
+ import setPopoverPosition from "../utils/setPopoverPosition";
3
+ import { MenuPopoutContextName, type MenuPopoutContext } from "./MenuTypes";
4
+
5
+ /**
6
+ * The content that is displayed for a MenuPopout when it is open.
7
+ *
8
+ * @prop {"top" | "right" | "bottom" | "left"} side -- Where the popout will be shown, relative to the popout button
9
+ * @prop {"start" | "center" | "end"} alignment -- How the popout will be aligned, relative to the popout button
10
+ */
11
+ export default function MenuPopoutContent() {
12
+ $props.side ??= "right";
13
+ $props.alignment ??= "start";
14
+
15
+ let div: HTMLDivElement;
16
+ let shown = false;
17
+
18
+ // Get stuff out of the MenuPopoutContext
19
+ const popoutContext = $context[MenuPopoutContextName] as MenuPopoutContext;
20
+ if (!popoutContext) {
21
+ throw new Error("MenuPopoutContent must be contained within a MenuPopout");
22
+ }
23
+ let $state = popoutContext.state;
24
+
25
+ $run(() => {
26
+ if (!$state.visible) {
27
+ hide();
28
+ }
29
+ });
30
+
31
+ function show() {
32
+ shown = true;
33
+
34
+ // Set the position and listen for window resize and scroll to reset the position
35
+ setPosition();
36
+ addEventListener("resize", setPosition);
37
+ addEventListener("scroll", setPosition);
38
+ if (popoutContext.focusFirstElement) {
39
+ popoutContext.focusFirstElement();
40
+ }
41
+ }
42
+
43
+ function hide() {
44
+ if (!shown) return;
45
+
46
+ // Stop listening for window resize and focus the anchor element per WAI guidelines
47
+ removeEventListener("resize", setPosition);
48
+ removeEventListener("scroll", setPosition);
49
+ if (popoutContext.anchorElement?.focus) {
50
+ popoutContext.anchorElement.focus();
51
+ }
52
+ }
53
+
54
+ function setPosition() {
55
+ if (!popoutContext.anchorElement) {
56
+ throw new Error("MenuPopoutContent must have an anchor element");
57
+ }
58
+
59
+ setPopoverPosition(popoutContext.anchorElement, div, $props.side, $props.alignment);
60
+ }
61
+
62
+ @render {
63
+ @if ($state.visible) {
64
+ <div
65
+ {$props.id}
66
+ class={`torp-menu-popout-content ${$props.class ?? ""}`.trim()}
67
+ &ref={div}
68
+ onmount={show}
69
+ >
70
+ <slot />
71
+ </div>
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,117 @@
1
+ import {
2
+ MenuContextName,
3
+ MenuPopoutContextName,
4
+ type MenuContext,
5
+ type MenuPopoutContext
6
+ } from "./MenuTypes";
7
+
8
+ /**
9
+ * A button that is placed in a Menu or MenuGroup and signals a response by a user.
10
+ *
11
+ * @prop {number} hoverDelay -- The delay before showing the popout, in milliseconds
12
+ * @prop {boolean} disabled -- Whether this button is disabled
13
+ */
14
+ export default function MenuPopoutTrigger() {
15
+ $props.hoverDelay ??= 500;
16
+ $props.disabled ??= false;
17
+
18
+ let button: HTMLButtonElement;
19
+ let interval: NodeJS.Timeout | undefined;
20
+
21
+ // Get stuff out of the MenuContext
22
+ const menuContext = $context[MenuContextName] as MenuContext;
23
+ if (!menuContext) {
24
+ throw new Error("MenuPopoutTrigger must be contained within a Menu");
25
+ }
26
+ const { registerItem, handleItemKey, handleItemFocus } = menuContext;
27
+
28
+ // Get stuff out of the MenuPopoutContext
29
+ const popoutContext = $context[MenuPopoutContextName] as MenuPopoutContext;
30
+ if (!popoutContext) {
31
+ throw new Error("MenuPopoutTrigger must be contained within a MenuPopout");
32
+ }
33
+ const { state: $state, setVisible } = popoutContext;
34
+
35
+ // Register this item with the parent Menu and store the index
36
+ const { index } = registerItem(setFocused);
37
+
38
+ // TODO: This is getting called before @render
39
+ //$mount(() => {
40
+ // // Set the anchor element to the button when it exists
41
+ // popoutContext.anchorElement = button;
42
+ //});
43
+
44
+ function setFocused() {
45
+ button.focus();
46
+ }
47
+
48
+ function handleClick() {
49
+ setVisible(true);
50
+ }
51
+
52
+ function handlePopoutKey(e: KeyboardEvent) {
53
+ switch (e.key) {
54
+ case "Right":
55
+ case "ArrowRight":
56
+ // TODO: This should be based on what side the menu shows on
57
+ e.preventDefault();
58
+ setVisible(true);
59
+ break;
60
+ default:
61
+ handleItemKey(e);
62
+ }
63
+ }
64
+
65
+ function handleMouseEnter() {
66
+ if ($props.disabled) return;
67
+
68
+ // Handle the hover after a delay
69
+ if (!interval) {
70
+ interval = setTimeout(() => setVisible(true), $props.hoverDelay);
71
+ }
72
+ }
73
+
74
+ function handleMouseLeave() {
75
+ if ($props.disabled) return;
76
+
77
+ // Prevent short hovers or touches from firing the event
78
+ if (interval) {
79
+ clearTimeout(interval);
80
+ interval = undefined;
81
+ }
82
+ }
83
+
84
+ function handleMouseMove() {
85
+ if ($props.disabled) return;
86
+
87
+ // Reset the interval on mouse move
88
+ if (interval) {
89
+ clearTimeout(interval);
90
+ interval = setTimeout(() => setVisible(true), $props.hoverDelay);
91
+ }
92
+ }
93
+
94
+ @render {
95
+ <button
96
+ {$props.id}
97
+ class={`torp-menu-popout-trigger ${$props.class ?? ""}`.trim()}
98
+ type="button"
99
+ {$props.disabled}
100
+ role="menuitem"
101
+ aria-haspopup="true"
102
+ aria-expanded={($state.visible ?? false).toString()}
103
+ aria-disabled={$props.disabled ? "true" : undefined}
104
+ data-disabled={$props.disabled ? "true" : undefined}
105
+ onkeydown={handlePopoutKey}
106
+ onclick={handleClick}
107
+ onfocus={() => handleItemFocus(index)}
108
+ onmouseenter={handleMouseEnter}
109
+ onmouseleave={handleMouseLeave}
110
+ onmousemove={handleMouseMove}
111
+ &ref={button}
112
+ onmount={(el) => popoutContext.anchorElement = el}
113
+ >
114
+ <slot />
115
+ </button>
116
+ }
117
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ MenuButtonContextName,
3
+ MenuContextName,
4
+ MenuRadioGroupContextName,
5
+ type MenuButtonContext,
6
+ type MenuContext,
7
+ type MenuRadioGroupContext
8
+ } from "./MenuTypes";
9
+
10
+ /**
11
+ * A button that is placed in a Menu or MenuGroup to allow the user to select
12
+ * from a number of options.
13
+ *
14
+ * @prop {string} value -- The value of the item
15
+ * @prop {boolean} checked -- Whether this item is checked
16
+ * @prop {boolean} disabled -- Whether this item is disabled
17
+ */
18
+ export default function MenuRadio() {
19
+ let $state = $watch({
20
+ checked: $props.checked,
21
+ });
22
+
23
+ $run(() => {
24
+ $props.checked = $state.checked;
25
+ });
26
+
27
+ let button: HTMLButtonElement;
28
+
29
+ // Get stuff out of the MenuContext
30
+ const menuContext = $context[MenuContextName] as MenuContext;
31
+ if (!menuContext) {
32
+ throw new Error("MenuRadio must be contained within a Menu");
33
+ }
34
+ const { registerItem, handleItemKey, handleItemFocus } = menuContext;
35
+
36
+ // Get stuff out of the MenuRadioGroupContext
37
+ const groupContext = $context[MenuRadioGroupContextName] as MenuRadioGroupContext;
38
+ if (!groupContext) {
39
+ throw new Error("MenuRadio must be contained within a MenuRadioGroup");
40
+ }
41
+ const { registerItemInGroup, toggleItem } = groupContext;
42
+
43
+ // Register this item with the parent Menu
44
+ const { index } = registerItem(setFocused);
45
+
46
+ // Register this item with the parent MenuRadioGroup
47
+ registerItemInGroup($props.value, setChecked);
48
+
49
+ // Set the context to pass down to items
50
+ let context: MenuButtonContext = {
51
+ state: $state
52
+ };
53
+ $context[MenuButtonContextName] = context;
54
+
55
+ function setFocused() {
56
+ button.focus();
57
+ }
58
+
59
+ function setChecked(value: boolean) {
60
+ $state.checked = value;
61
+ }
62
+
63
+ function handleClick() {
64
+ if (!$state.checked) {
65
+ toggleItem($props.value);
66
+ }
67
+ }
68
+
69
+ @render {
70
+ <button
71
+ {$props.id}
72
+ class={`torp-menu-radio ${$props.class ?? ""}`.trim()}
73
+ type="button"
74
+ {$props.disabled}
75
+ role="menuitemradio"
76
+ aria-checked={$props.checked}
77
+ aria-disabled={$props.disabled ? "true" : undefined}
78
+ data-state={$props.checked ? "checked" : "unchecked"}
79
+ data-disabled={$props.disabled ? "true" : undefined}
80
+ onkeydown={handleItemKey}
81
+ onclick={handleClick}
82
+ onfocus={() => handleItemFocus(index)}
83
+ &ref={button}
84
+ >
85
+ <slot />
86
+ </button>
87
+ }
88
+ }