@torpor/ui 0.0.16 → 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 (156) hide show
  1. package/CHANGELOG.md +38 -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 +36 -0
  52. package/dist/TabGroup/TabGroup.torp +225 -0
  53. package/dist/TabGroup/TabGroupTypes.ts +39 -0
  54. package/dist/TabGroup/TabHeader.torp +62 -0
  55. package/dist/TabGroup/TabList.torp +50 -0
  56. package/dist/TabGroup/TabTrigger.torp +65 -0
  57. package/dist/TabGroup/index.cjs +53 -0
  58. package/dist/TabGroup/index.cjs.map +1 -0
  59. package/dist/TabGroup/index.js +22 -0
  60. package/dist/TabGroup/index.js.map +1 -0
  61. package/dist/metafile-cjs.json +1 -1
  62. package/dist/metafile-esm.json +1 -1
  63. package/dist/mount/disableScroll.ts +28 -0
  64. package/dist/mount/hoverTrigger.ts +59 -0
  65. package/dist/mount/trapFocus.ts +58 -0
  66. package/dist/utils/documentEvents.ts +17 -0
  67. package/dist/utils/getScrollParent.ts +22 -0
  68. package/dist/utils/setContextPosition.ts +31 -0
  69. package/dist/utils/setPopoverPosition.ts +138 -0
  70. package/package.json +26 -6
  71. package/scripts/postbuild.ts +28 -10
  72. package/src/Accordion/Accordion.torp +53 -54
  73. package/src/Accordion/AccordionContent.torp +5 -5
  74. package/src/Accordion/AccordionHeader.torp +5 -4
  75. package/src/Accordion/AccordionItem.torp +13 -8
  76. package/src/Accordion/AccordionTrigger.torp +5 -5
  77. package/src/Accordion/AccordionTypes.ts +1 -0
  78. package/src/Dialog/Dialog.torp +156 -0
  79. package/src/Dialog/DialogBody.torp +34 -0
  80. package/src/Dialog/DialogButton.torp +48 -0
  81. package/src/Dialog/DialogFooter.torp +13 -0
  82. package/src/Dialog/DialogHeader.torp +34 -0
  83. package/src/Dialog/DialogTypes.ts +16 -0
  84. package/src/Dialog/index.ts +7 -0
  85. package/src/Dialog.ts +7 -0
  86. package/src/NavMenu/NavMenu.torp +178 -0
  87. package/src/NavMenu/NavMenuButton.torp +46 -0
  88. package/src/NavMenu/NavMenuElement.torp +43 -0
  89. package/src/NavMenu/NavMenuGroup.torp +30 -0
  90. package/src/NavMenu/NavMenuGroupHeader.torp +30 -0
  91. package/src/NavMenu/NavMenuLink.torp +52 -0
  92. package/src/NavMenu/NavMenuPopout.torp +69 -0
  93. package/src/NavMenu/NavMenuPopoutButton.torp +120 -0
  94. package/src/NavMenu/NavMenuPopoutContent.torp +79 -0
  95. package/src/NavMenu/NavMenuSeparator.torp +14 -0
  96. package/src/NavMenu/NavMenuTypes.ts +32 -0
  97. package/src/NavMenu/index.ts +23 -0
  98. package/src/NavMenu.ts +23 -0
  99. package/src/Popover/Popover.torp +105 -0
  100. package/src/Popover/PopoverContentAnchored.torp +107 -0
  101. package/src/Popover/PopoverContentContextual.torp +99 -0
  102. package/src/Popover/PopoverContentStatic.torp +78 -0
  103. package/src/Popover/PopoverOverlay.torp +32 -0
  104. package/src/Popover/PopoverTrigger.torp +29 -0
  105. package/src/Popover/PopoverTriggerContextual.torp +37 -0
  106. package/src/Popover/PopoverTriggerHover.torp +30 -0
  107. package/src/Popover/PopoverTypes.ts +30 -0
  108. package/src/Popover/index.ts +29 -0
  109. package/src/Popover/mountClickPopover.ts +50 -0
  110. package/src/Popover/mountContextPopover.ts +50 -0
  111. package/src/Popover/mountHoverPopover.ts +57 -0
  112. package/src/Popover/showPopover.ts +24 -0
  113. package/src/Popover/showPopoverPrompt.ts +23 -0
  114. package/src/Popover.ts +29 -0
  115. package/src/TabGroup/TabContent.torp +36 -0
  116. package/src/TabGroup/TabGroup.torp +225 -0
  117. package/src/TabGroup/TabGroupTypes.ts +39 -0
  118. package/src/TabGroup/TabHeader.torp +62 -0
  119. package/src/TabGroup/TabList.torp +50 -0
  120. package/src/TabGroup/TabTrigger.torp +65 -0
  121. package/src/TabGroup/index.ts +7 -0
  122. package/src/TabGroup.ts +7 -0
  123. package/src/mount/disableScroll.ts +28 -0
  124. package/src/mount/hoverTrigger.ts +59 -0
  125. package/src/mount/trapFocus.ts +58 -0
  126. package/src/utils/documentEvents.ts +17 -0
  127. package/src/utils/getScrollParent.ts +22 -0
  128. package/src/utils/setContextPosition.ts +31 -0
  129. package/src/utils/setPopoverPosition.ts +138 -0
  130. package/test/Accordion/accessibility.test.ts +0 -3
  131. package/test/Accordion/for-items.test.ts +2 -4
  132. package/test/Accordion/keyboard.test.ts +0 -3
  133. package/test/Accordion/multiple-item-toggle.test.ts +3 -5
  134. package/test/Accordion/single-item-toggle.test.ts +3 -5
  135. package/test/Dialog/accessibility-alert.test.ts +40 -0
  136. package/test/Dialog/accessibility-modal.test.ts +52 -0
  137. package/test/Dialog/components/CustomDialog.torp +20 -0
  138. package/test/Dialog/components/DialogAccessibility.torp +31 -0
  139. package/test/Dialog/components/DialogFunction.torp +22 -0
  140. package/test/Dialog/components/DialogVariable.torp +35 -0
  141. package/test/Dialog/function.test.ts +26 -0
  142. package/test/Dialog/variable.test.ts +27 -0
  143. package/test/NavMenu/accessibility.test.ts +97 -0
  144. package/test/NavMenu/basic.test.ts +22 -0
  145. package/test/NavMenu/components/NavMenu.torp +28 -0
  146. package/test/NavMenu/components/NavMenuAccessibility.torp +35 -0
  147. package/test/TabGroup/accessibility.test.ts +67 -0
  148. package/test/TabGroup/components/TabGroupAccessibility.torp +28 -0
  149. package/test/TabGroup/components/TabGroupKeyboard.torp +28 -0
  150. package/test/TabGroup/components/TabGroupList.torp +26 -0
  151. package/test/TabGroup/components/TabGroupTrigger.torp +28 -0
  152. package/test/TabGroup/for-items.test.ts +52 -0
  153. package/test/TabGroup/item-trigger.test.ts +32 -0
  154. package/test/TabGroup/item-value.test.ts +17 -0
  155. package/test/TabGroup/keyboard.test.ts +72 -0
  156. package/tsup.config.ts +8 -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torpor/ui",
3
- "version": "0.0.16",
3
+ "version": "0.1.0",
4
4
  "description": "Accessible, unstyled components for use in your Torpor front-end",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -12,6 +12,26 @@
12
12
  "import": "./dist/Accordion/index.js",
13
13
  "require": "./dist/Accordion/index.cjs"
14
14
  },
15
+ "./Dialog": {
16
+ "types": "./dist/Dialog/index.d.ts",
17
+ "import": "./dist/Dialog/index.js",
18
+ "require": "./dist/Dialog/index.cjs"
19
+ },
20
+ "./NavMenu": {
21
+ "types": "./dist/NavMenu/index.d.ts",
22
+ "import": "./dist/NavMenu/index.js",
23
+ "require": "./dist/NavMenu/index.cjs"
24
+ },
25
+ "./Popover": {
26
+ "types": "./dist/Popover/index.d.ts",
27
+ "import": "./dist/Popover/index.js",
28
+ "require": "./dist/Popover/index.cjs"
29
+ },
30
+ "./TabGroup": {
31
+ "types": "./dist/TabGroup/index.d.ts",
32
+ "import": "./dist/TabGroup/index.js",
33
+ "require": "./dist/TabGroup/index.cjs"
34
+ },
15
35
  "./motion": {
16
36
  "types": "./dist/motion.d.ts",
17
37
  "import": "./dist/motion.js",
@@ -30,18 +50,18 @@
30
50
  "@testing-library/user-event": "^14.6.1",
31
51
  "@trivago/prettier-plugin-sort-imports": "^5.2.2",
32
52
  "@types/jsdom": "^21.1.7",
33
- "@types/node": "^22.15.17",
53
+ "@types/node": "^22.15.21",
34
54
  "chalk": "^5.4.1",
35
55
  "esno": "^4.8.0",
36
56
  "fast-glob": "^3.3.3",
37
57
  "jsdom": "^26.1.0",
38
- "tsup": "^8.4.0",
58
+ "tsup": "^8.5.0",
39
59
  "typescript": "^5.8.3",
40
- "vitest": "^3.1.3"
60
+ "vitest": "^3.1.4"
41
61
  },
42
62
  "dependencies": {
43
- "@torpor/view": "^0.0.13",
44
- "@torpor/unplugin": "^0.1.9"
63
+ "@torpor/view": "^0.1.0",
64
+ "@torpor/unplugin": "^0.1.11"
45
65
  },
46
66
  "scripts": {
47
67
  "build": "tsc --noEmit && tsup",
@@ -1,13 +1,18 @@
1
1
  import chalk from "chalk";
2
2
  import { existsSync, promises as fs, lstatSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  // HACK: There is almost certainly a way to do this with standard TS/TSUP/etc
7
7
  // but I can't figure it out
8
8
 
9
9
  async function run() {
10
- const distFolder = resolve(dirname(fileURLToPath(import.meta.url)), "../dist");
10
+ const scriptsFolder = dirname(fileURLToPath(import.meta.url));
11
+ const sourceFolder = resolve(scriptsFolder, "../src");
12
+ const distFolder = resolve(scriptsFolder, "../dist");
13
+
14
+ // Read the dist files first, not for every component
15
+ const distFiles = (await fs.readdir(distFolder)).filter((f) => f.endsWith(".torp"));
11
16
 
12
17
  // Loop through component folders e.g. Accordion
13
18
  const componentFolders = (await fs.readdir(distFolder)).filter((f) =>
@@ -27,10 +32,17 @@ async function run() {
27
32
  }
28
33
 
29
34
  // Move all *.torp files for the component into this folder
30
- const relatedFiles = (await fs.readdir(distFolder)).filter(
31
- (f) => f.startsWith(component) && f !== component,
32
- );
33
- for (const file of relatedFiles) {
35
+ let relatedFiles = (await fs.readdir(join(sourceFolder, component)))
36
+ .filter((f) => f.endsWith(".torp") && f !== component)
37
+ .map((f) => basename(f, ".torp"))
38
+ .map((f) => {
39
+ const output = distFiles.find((df) => df.startsWith(`${f}-`) && df.endsWith(".torp"));
40
+ if (!output) {
41
+ console.log("FILE NOT FOUND:", f, distFiles);
42
+ }
43
+ return output!;
44
+ });
45
+ for (let file of relatedFiles) {
34
46
  const newfile = file.substring(0, file.indexOf("-")) + ".torp";
35
47
  await fs.rename(join(distFolder, file), join(distFolder, component, newfile));
36
48
  }
@@ -46,16 +58,22 @@ async function run() {
46
58
  }
47
59
 
48
60
  // Move all utils/*.ts files into the dest folder
49
- if (!existsSync(join(distFolder, "utils"))) {
50
- await fs.mkdir(join(distFolder, "utils"));
61
+ moveUtils(distFolder, "utils");
62
+ moveUtils(distFolder, "mount");
63
+ }
64
+
65
+ async function moveUtils(distFolder: string, folderName: string) {
66
+ // Move all utils/*.ts files into the dest folder
67
+ if (!existsSync(join(distFolder, folderName))) {
68
+ await fs.mkdir(join(distFolder, folderName));
51
69
  }
52
70
 
53
- const utilsFolder = resolve(distFolder, "..", "src", "utils");
71
+ const utilsFolder = resolve(distFolder, "..", "src", folderName);
54
72
  const utilsFiles = (await fs.readdir(utilsFolder)).filter(
55
73
  (f) => f.endsWith(".ts") && f !== "index.ts",
56
74
  );
57
75
  for (const file of utilsFiles) {
58
- await fs.copyFile(join(utilsFolder, file), join(distFolder, "utils", file));
76
+ await fs.copyFile(join(utilsFolder, file), join(distFolder, folderName, file));
59
77
  }
60
78
  }
61
79
 
@@ -1,4 +1,4 @@
1
- import { AccordionContextName, type AccordionContext, type ItemState } from './AccordionTypes';
1
+ import { AccordionContextName, type AccordionContext, type ItemState } from "./AccordionTypes";
2
2
 
3
3
  /**
4
4
  * A vertically stacked set of interactive headings that enable users to reveal
@@ -19,24 +19,16 @@ import { AccordionContextName, type AccordionContext, type ItemState } from './A
19
19
  * </Accordion>
20
20
  * ```
21
21
  *
22
- * @prop {'single' | 'multiple'} type -- Whether a single AccordionItem or multiple AccordionItems can be expanded at the same time
23
- * @prop {string | string[]} value -- The value of the expanded AccordionItem, or an array of values if the type is 'multiple'
22
+ * @prop {"single" | "multiple"} type -- Whether a single AccordionItem or multiple AccordionItems can be expanded at the same time
23
+ * @prop {string | string[]} value -- The value of the expanded AccordionItem, or an array of values if the type is "multiple"
24
24
  * @prop {boolean} disabled -- If set to true, the user cannot interact with any items in the Accordion
25
25
  * @prop {(value: string | string[]) => void} [onchange] -- Raised when the expanded item(s) have changed
26
26
  */
27
27
  export default function Accordion($props: any) {
28
- $props.type ||= 'single';
29
- $props.value ||= $props.type === 'single' ? '' : [];
28
+ $props.type ||= "single";
29
+ $props.value ||= $props.type === "single" ? "" : [];
30
30
 
31
- interface State {
32
- value: string | string[];
33
- itemStates: ItemState[];
34
- }
35
-
36
- let $state: State = $watch({
37
- value: $props.value,
38
- itemStates: []
39
- })
31
+ let itemStates: ItemState[] = [];
40
32
 
41
33
  // This is used to assign automatic indexes to items where needed
42
34
  let nextIndex = 0;
@@ -52,24 +44,24 @@ export default function Accordion($props: any) {
52
44
 
53
45
  $run(() => {
54
46
  // HACK: This should probably be done with setters?
55
- let newValue = $state.value;
47
+ let newValue = $props.value;
56
48
 
57
49
  // Get the value
58
50
  switch ($props.type) {
59
- case 'single': {
51
+ case "single": {
60
52
  // Ensure the value passed in was a string (in case e.g. the user has set values to ints)
61
53
  newValue = newValue?.toString();
62
- for (let item of $state.itemStates) {
54
+ for (let item of itemStates) {
63
55
  item.expanded = item.value === newValue;
64
56
  }
65
57
  break;
66
58
  }
67
- case 'multiple': {
59
+ case "multiple": {
68
60
  // Ensure the value passed in is an array
69
61
  if (!Array.isArray(newValue)) {
70
62
  newValue = [newValue];
71
63
  }
72
- for (let item of $state.itemStates) {
64
+ for (let item of itemStates) {
73
65
  item.expanded = newValue.indexOf(item.value) !== -1;
74
66
  }
75
67
  break;
@@ -77,20 +69,24 @@ export default function Accordion($props: any) {
77
69
  }
78
70
 
79
71
  // Set the value
80
- $state.value = newValue;
72
+ $props.value = newValue;
81
73
  })
82
74
 
83
75
  $run(() => {
84
- $state.itemStates.forEach((s) => s.parentDisabled = $props.disabled);
76
+ itemStates.forEach((s) => s.parentDisabled = $props.disabled);
85
77
  });
86
78
 
87
79
  // Dispatch the change event when the value changes
88
- // TODO: Should this be a setter instead?
80
+ let firstChange = true;
89
81
  $run(() => {
90
- if ($props.onchange) {
91
- $props.onchange($state.value);
82
+ const ev = $props.onchange;
83
+ const value = $props.value;
84
+ if (firstChange) {
85
+ firstChange = false;
86
+ } else if (ev) {
87
+ ev(value);
92
88
  }
93
- })
89
+ });
94
90
 
95
91
  function registerItem(itemState: ItemState) {
96
92
  // If an index wasn't set for the item, set it automatically
@@ -104,52 +100,52 @@ export default function Accordion($props: any) {
104
100
  }
105
101
 
106
102
  // If we have a value, set whether the item should be expanded
107
- if ($state.value) {
103
+ if ($props.value) {
108
104
  switch ($props.type) {
109
- case 'single': {
110
- itemState.expanded = $state.value === itemState.value;
105
+ case "single": {
106
+ itemState.expanded = $props.value === itemState.value;
111
107
  break;
112
108
  }
113
- case 'multiple': {
114
- itemState.expanded = $state.value.includes(itemState.value);
109
+ case "multiple": {
110
+ itemState.expanded = $props.value.includes(itemState.value);
115
111
  break;
116
112
  }
117
113
  }
118
114
  }
119
115
 
120
116
  // Set whether the Accordion itself is disabled
121
- $state.parentDisabled = $props.disabled
117
+ itemState.parentDisabled = $props.disabled
122
118
 
123
119
  // Add the item's state to the array
124
- $state.itemStates.splice(itemState.index, 0, itemState);
120
+ itemStates.splice(itemState.index, 0, itemState);
125
121
  }
126
122
 
127
123
  // TODO: I think this should be done by value
128
124
  function removeItem(itemIndex: number) {
129
125
  // Remove the item and re-index
130
- let index = $state.itemStates.findIndex((item) => item.index === itemIndex);
126
+ let index = itemStates.findIndex((item) => item.index === itemIndex);
131
127
  if (index !== -1) {
132
- $state.itemStates.splice(itemIndex, 1);
128
+ itemStates.splice(itemIndex, 1);
133
129
  }
134
- $state.itemStates.forEach((item, i) => (item.index = i));
130
+ itemStates.forEach((item, i) => (item.index = i));
135
131
  }
136
132
 
137
133
  function toggleItem(toggleValue: string) {
138
134
  switch ($props.type) {
139
- case 'single': {
140
- for (let item of $state.itemStates) {
135
+ case "single": {
136
+ for (let item of itemStates) {
141
137
  // The item is expanded if it has the supplied value (i.e. if it was clicked) and if it
142
138
  // wasn't already expanded (i.e. we collapse it if it was expanded and clicked)
143
139
  if (item.value === toggleValue) {
144
- $state.value = item.expanded ? '' : item.value;
140
+ $props.value = item.expanded ? "" : item.value;
145
141
  break;
146
142
  }
147
143
  }
148
144
  break;
149
145
  }
150
- case 'multiple': {
146
+ case "multiple": {
151
147
  let newValue = [];
152
- for (let item of $state.itemStates) {
148
+ for (let item of itemStates) {
153
149
  // The item is expanded if it has the supplied value (i.e. if it was clicked) or if it
154
150
  // was already expanded
155
151
  if (
@@ -159,7 +155,7 @@ export default function Accordion($props: any) {
159
155
  newValue.push(item.value);
160
156
  }
161
157
  }
162
- $state.value = newValue.sort();
158
+ $props.value = newValue.sort();
163
159
  break;
164
160
  }
165
161
  }
@@ -167,33 +163,33 @@ export default function Accordion($props: any) {
167
163
 
168
164
  function handleHeaderKey(headerIndex: number, e: KeyboardEvent) {
169
165
  switch (e.key) {
170
- case 'Up':
171
- case 'ArrowUp': {
166
+ case "Up":
167
+ case "ArrowUp": {
172
168
  if (headerIndex > 0) {
173
169
  e.preventDefault();
174
- $state.itemStates[headerIndex - 1].setFocused();
170
+ itemStates[headerIndex - 1].setFocused();
175
171
  }
176
172
  break;
177
173
  }
178
- case 'Down':
179
- case 'ArrowDown': {
180
- if (headerIndex < $state.itemStates.length - 1) {
174
+ case "Down":
175
+ case "ArrowDown": {
176
+ if (headerIndex < itemStates.length - 1) {
181
177
  e.preventDefault();
182
- $state.itemStates[headerIndex + 1].setFocused();
178
+ itemStates[headerIndex + 1].setFocused();
183
179
  }
184
180
  break;
185
181
  }
186
- case 'Home': {
182
+ case "Home": {
187
183
  if (headerIndex > 0) {
188
184
  e.preventDefault();
189
- $state.itemStates[0].setFocused();
185
+ itemStates[0].setFocused();
190
186
  }
191
187
  break;
192
188
  }
193
- case 'End': {
194
- if (headerIndex < $state.itemStates.length - 1) {
189
+ case "End": {
190
+ if (headerIndex < itemStates.length - 1) {
195
191
  e.preventDefault();
196
- $state.itemStates[$state.itemStates.length - 1].setFocused();
192
+ itemStates[itemStates.length - 1].setFocused();
197
193
  }
198
194
  break;
199
195
  }
@@ -201,7 +197,10 @@ export default function Accordion($props: any) {
201
197
  }
202
198
 
203
199
  @render {
204
- <div class="torp-accordion">
200
+ <div
201
+ id={$props.id}
202
+ class={`torp-accordion ${$props.class ?? ""}`.trim()}
203
+ >
205
204
  <:slot />
206
205
  </div>
207
206
  }
@@ -1,5 +1,5 @@
1
- import getId from '../utils/getId';
2
- import { AccordionItemContextName, type AccordionItemContext } from './AccordionTypes';
1
+ import getId from "../utils/getId";
2
+ import { AccordionItemContextName, type AccordionItemContext } from "./AccordionTypes";
3
3
 
4
4
  /**
5
5
  * Content for an AccordionItem, which can be revealed or hidden by an associated AccordionTrigger.
@@ -12,7 +12,7 @@ export default function AccordionContent($props: any) {
12
12
 
13
13
  const context = $context[AccordionItemContextName] as AccordionItemContext;
14
14
  if (!context) {
15
- throw new Error('AccordionContent must be contained within an AccordionItem');
15
+ throw new Error("AccordionContent must be contained within an AccordionItem");
16
16
  }
17
17
  let $state = $context[AccordionItemContextName].state
18
18
 
@@ -22,8 +22,8 @@ export default function AccordionContent($props: any) {
22
22
  @render {
23
23
  <div
24
24
  id={$props.id}
25
- class='torp-accordion-content'
26
- role={$props.ariaRegion ? 'region' : undefined}
25
+ class={`torp-accordion-content ${$props.class ?? ""}`.trim()}
26
+ role={$props.ariaRegion ? "region" : undefined}
27
27
  aria-labelledby={$state.headerId}
28
28
  data-state={$state.dataState}
29
29
  data-disabled={$state.dataDisabled}
@@ -1,4 +1,4 @@
1
- import { AccordionItemContextName, type AccordionItemContext } from './AccordionTypes';
1
+ import { AccordionItemContextName, type AccordionItemContext } from "./AccordionTypes";
2
2
 
3
3
  /**
4
4
  * The header for an AccordionItem, which contains the AccordionTrigger.
@@ -11,19 +11,20 @@ import { AccordionItemContextName, type AccordionItemContext } from './Accordion
11
11
  * </AccordionHeader>
12
12
  * ```
13
13
  *
14
- * @prop {'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'} as -- The type of HTML header to use
14
+ * @prop {"h1" | "h2" | "h3" | "h4" | "h5" | "h6"} as -- The type of HTML header to use
15
15
  */
16
16
  export default function AccordionHeader($props: any) {
17
17
  const context = $context[AccordionItemContextName] as AccordionItemContext;
18
18
  if (!context) {
19
- throw new Error('AccordionHeader must be contained within an AccordionItem');
19
+ throw new Error("AccordionHeader must be contained within an AccordionItem");
20
20
  }
21
21
  let $state = $context[AccordionItemContextName].state
22
22
 
23
23
  @render {
24
24
  @// TODO: $props.as
25
25
  <h4
26
- class='torp-accordion-header'
26
+ id={$props.id}
27
+ class={`torp-accordion-header ${$props.class ?? ""}`.trim()}
27
28
  data-state={$state.dataState}
28
29
  data-disabled={$state.dataDisabled}
29
30
  >
@@ -1,4 +1,4 @@
1
- import { AccordionContextName, AccordionItemContextName, type AccordionContext, type AccordionItemContext } from './AccordionTypes';
1
+ import { AccordionContextName, AccordionItemContextName, type AccordionContext, type AccordionItemContext } from "./AccordionTypes";
2
2
 
3
3
  /**
4
4
  * An item in an Accordion, which must consist of an AccordionTrigger and an AccordionContent.
@@ -27,14 +27,14 @@ export default function AccordionItem($props: any) {
27
27
  disabled: $props.disabled,
28
28
  parentDisabled: false,
29
29
  get dataState() {
30
- return this.expanded ? 'open' : 'closed';
30
+ return this.expanded ? "open" : "closed";
31
31
  },
32
32
  })
33
33
 
34
34
  // Get stuff out of the AccordionContext
35
35
  const context = $context[AccordionContextName] as AccordionContext;
36
36
  if (!context) {
37
- throw new Error('AccordionItem must be contained within an Accordion');
37
+ throw new Error("AccordionItem must be contained within an Accordion");
38
38
  }
39
39
 
40
40
  // Put stuff into an AccordionItemContext
@@ -56,16 +56,21 @@ export default function AccordionItem($props: any) {
56
56
  });
57
57
 
58
58
  // Dispatch the toggle event when the value of expanded changes
59
- // TODO: Should this be a setter instead?
59
+ let firstToggle = true;
60
60
  $run(() => {
61
- if ($props.ontoggle) {
62
- $props.ontoggle($state.expanded);
61
+ const ev = $props.ontoggle;
62
+ const expanded = $state.expanded;
63
+ if (firstToggle) {
64
+ firstToggle = false;
65
+ } else if (ev) {
66
+ ev(expanded);
63
67
  }
64
- })
68
+ });
65
69
 
66
70
  @render {
67
71
  <div
68
- class='torp-accordion-item'
72
+ id={$props.id}
73
+ class={`torp-accordion-item ${$props.class ?? ""}`.trim()}
69
74
  data-state={$state.dataState}
70
75
  data-disabled={$state.dataDisabled}
71
76
  >
@@ -1,5 +1,5 @@
1
- import getId from '../utils/getId';
2
- import { AccordionItemContextName, type AccordionItemContext } from './AccordionTypes';
1
+ import getId from "../utils/getId";
2
+ import { AccordionItemContextName, type AccordionItemContext } from "./AccordionTypes";
3
3
 
4
4
  /**
5
5
  * A header button for an AccordionItem, which can be used to reveal or hide an associated AccordionContent.
@@ -12,7 +12,7 @@ export default function AccordionTrigger() {
12
12
  // Get stuff out of the AccordionItemContext
13
13
  const context = $context[AccordionItemContextName] as AccordionItemContext;
14
14
  if (!context) {
15
- throw new Error('AccordionTrigger must be contained within an AccordionItem');
15
+ throw new Error("AccordionTrigger must be contained within an AccordionItem");
16
16
  }
17
17
  let $state = $context[AccordionItemContextName].state;
18
18
 
@@ -42,14 +42,14 @@ export default function AccordionTrigger() {
42
42
  <button
43
43
  type="button"
44
44
  id={$props.id}
45
- class='torp-accordion-button'
45
+ class={`torp-accordion-trigger ${$props.class ?? ""}`.trim()}
46
46
  disabled={$state.disabled || $state.parentDisabled}
47
47
  aria-expanded={$state.expanded.toString()}
48
48
  aria-disabled={$state.disabled}
49
49
  aria-controls={$state.contentId}
50
50
  data-state={$state.dataState}
51
51
  data-disabled={$state.disabled}
52
- :self={button}
52
+ &ref={button}
53
53
  onclick={handleClick}
54
54
  onkeydown={handleKey}
55
55
  >
@@ -27,6 +27,7 @@ export interface ItemState {
27
27
  expanded: boolean;
28
28
  disabled: boolean;
29
29
  parentDisabled: boolean;
30
+ dataState: "open" | "closed";
30
31
  /** Called in the Accordion when a header needs to be focused */
31
32
  setFocused: () => void;
32
33
  }