@fristil/designsystem 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 (314) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/dist/cli.d.ts +31 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +261 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/components/css/accordion/accordion.d.ts +37 -0
  9. package/dist/components/css/accordion/accordion.d.ts.map +1 -0
  10. package/dist/components/css/accordion/accordion.js +31 -0
  11. package/dist/components/css/accordion/accordion.js.map +1 -0
  12. package/dist/components/css/alert/alert.d.ts +35 -0
  13. package/dist/components/css/alert/alert.d.ts.map +1 -0
  14. package/dist/components/css/alert/alert.js +29 -0
  15. package/dist/components/css/alert/alert.js.map +1 -0
  16. package/dist/components/css/avatar/avatar.d.ts +40 -0
  17. package/dist/components/css/avatar/avatar.d.ts.map +1 -0
  18. package/dist/components/css/avatar/avatar.js +30 -0
  19. package/dist/components/css/avatar/avatar.js.map +1 -0
  20. package/dist/components/css/badge/badge.d.ts +24 -0
  21. package/dist/components/css/badge/badge.d.ts.map +1 -0
  22. package/dist/components/css/badge/badge.js +24 -0
  23. package/dist/components/css/badge/badge.js.map +1 -0
  24. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +25 -0
  25. package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
  26. package/dist/components/css/breadcrumbs/breadcrumbs.js +23 -0
  27. package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
  28. package/dist/components/css/button/button.d.ts +26 -0
  29. package/dist/components/css/button/button.d.ts.map +1 -0
  30. package/dist/components/css/button/button.js +25 -0
  31. package/dist/components/css/button/button.js.map +1 -0
  32. package/dist/components/css/card/card.d.ts +36 -0
  33. package/dist/components/css/card/card.d.ts.map +1 -0
  34. package/dist/components/css/card/card.js +28 -0
  35. package/dist/components/css/card/card.js.map +1 -0
  36. package/dist/components/css/checkbox/checkbox.d.ts +32 -0
  37. package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
  38. package/dist/components/css/checkbox/checkbox.js +27 -0
  39. package/dist/components/css/checkbox/checkbox.js.map +1 -0
  40. package/dist/components/css/dialog/dialog.d.ts +30 -0
  41. package/dist/components/css/dialog/dialog.d.ts.map +1 -0
  42. package/dist/components/css/dialog/dialog.js +28 -0
  43. package/dist/components/css/dialog/dialog.js.map +1 -0
  44. package/dist/components/css/divider/divider.d.ts +28 -0
  45. package/dist/components/css/divider/divider.d.ts.map +1 -0
  46. package/dist/components/css/divider/divider.js +22 -0
  47. package/dist/components/css/divider/divider.js.map +1 -0
  48. package/dist/components/css/error-text/error-text.d.ts +24 -0
  49. package/dist/components/css/error-text/error-text.d.ts.map +1 -0
  50. package/dist/components/css/error-text/error-text.js +18 -0
  51. package/dist/components/css/error-text/error-text.js.map +1 -0
  52. package/dist/components/css/fieldset/fieldset.d.ts +55 -0
  53. package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
  54. package/dist/components/css/fieldset/fieldset.js +39 -0
  55. package/dist/components/css/fieldset/fieldset.js.map +1 -0
  56. package/dist/components/css/file-upload/file-upload.d.ts +41 -0
  57. package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
  58. package/dist/components/css/file-upload/file-upload.js +32 -0
  59. package/dist/components/css/file-upload/file-upload.js.map +1 -0
  60. package/dist/components/css/heading/heading.d.ts +29 -0
  61. package/dist/components/css/heading/heading.d.ts.map +1 -0
  62. package/dist/components/css/heading/heading.js +23 -0
  63. package/dist/components/css/heading/heading.js.map +1 -0
  64. package/dist/components/css/help-text/help-text.d.ts +24 -0
  65. package/dist/components/css/help-text/help-text.d.ts.map +1 -0
  66. package/dist/components/css/help-text/help-text.js +23 -0
  67. package/dist/components/css/help-text/help-text.js.map +1 -0
  68. package/dist/components/css/input/input.d.ts +39 -0
  69. package/dist/components/css/input/input.d.ts.map +1 -0
  70. package/dist/components/css/input/input.js +44 -0
  71. package/dist/components/css/input/input.js.map +1 -0
  72. package/dist/components/css/label/label.d.ts +33 -0
  73. package/dist/components/css/label/label.d.ts.map +1 -0
  74. package/dist/components/css/label/label.js +23 -0
  75. package/dist/components/css/label/label.js.map +1 -0
  76. package/dist/components/css/link/link.d.ts +21 -0
  77. package/dist/components/css/link/link.d.ts.map +1 -0
  78. package/dist/components/css/link/link.js +14 -0
  79. package/dist/components/css/link/link.js.map +1 -0
  80. package/dist/components/css/list/list.d.ts +29 -0
  81. package/dist/components/css/list/list.d.ts.map +1 -0
  82. package/dist/components/css/list/list.js +23 -0
  83. package/dist/components/css/list/list.js.map +1 -0
  84. package/dist/components/css/pagination/pagination.d.ts +27 -0
  85. package/dist/components/css/pagination/pagination.d.ts.map +1 -0
  86. package/dist/components/css/pagination/pagination.js +25 -0
  87. package/dist/components/css/pagination/pagination.js.map +1 -0
  88. package/dist/components/css/paragraph/paragraph.d.ts +36 -0
  89. package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
  90. package/dist/components/css/paragraph/paragraph.js +26 -0
  91. package/dist/components/css/paragraph/paragraph.js.map +1 -0
  92. package/dist/components/css/radio/radio.d.ts +32 -0
  93. package/dist/components/css/radio/radio.d.ts.map +1 -0
  94. package/dist/components/css/radio/radio.js +27 -0
  95. package/dist/components/css/radio/radio.js.map +1 -0
  96. package/dist/components/css/search/search.d.ts +33 -0
  97. package/dist/components/css/search/search.d.ts.map +1 -0
  98. package/dist/components/css/search/search.js +31 -0
  99. package/dist/components/css/search/search.js.map +1 -0
  100. package/dist/components/css/select/select.d.ts +23 -0
  101. package/dist/components/css/select/select.d.ts.map +1 -0
  102. package/dist/components/css/select/select.js +18 -0
  103. package/dist/components/css/select/select.js.map +1 -0
  104. package/dist/components/css/shared.d.ts +26 -0
  105. package/dist/components/css/shared.d.ts.map +1 -0
  106. package/dist/components/css/shared.js +34 -0
  107. package/dist/components/css/shared.js.map +1 -0
  108. package/dist/components/css/skeleton/skeleton.d.ts +29 -0
  109. package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
  110. package/dist/components/css/skeleton/skeleton.js +23 -0
  111. package/dist/components/css/skeleton/skeleton.js.map +1 -0
  112. package/dist/components/css/skip-link/skip-link.d.ts +18 -0
  113. package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
  114. package/dist/components/css/skip-link/skip-link.js +16 -0
  115. package/dist/components/css/skip-link/skip-link.js.map +1 -0
  116. package/dist/components/css/spinner/spinner.d.ts +33 -0
  117. package/dist/components/css/spinner/spinner.d.ts.map +1 -0
  118. package/dist/components/css/spinner/spinner.js +25 -0
  119. package/dist/components/css/spinner/spinner.js.map +1 -0
  120. package/dist/components/css/sr-only/sr-only.d.ts +17 -0
  121. package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
  122. package/dist/components/css/sr-only/sr-only.js +14 -0
  123. package/dist/components/css/sr-only/sr-only.js.map +1 -0
  124. package/dist/components/css/switch/switch.d.ts +33 -0
  125. package/dist/components/css/switch/switch.d.ts.map +1 -0
  126. package/dist/components/css/switch/switch.js +29 -0
  127. package/dist/components/css/switch/switch.js.map +1 -0
  128. package/dist/components/css/table/table.d.ts +36 -0
  129. package/dist/components/css/table/table.d.ts.map +1 -0
  130. package/dist/components/css/table/table.js +28 -0
  131. package/dist/components/css/table/table.js.map +1 -0
  132. package/dist/components/css/tag/tag.d.ts +35 -0
  133. package/dist/components/css/tag/tag.d.ts.map +1 -0
  134. package/dist/components/css/tag/tag.js +27 -0
  135. package/dist/components/css/tag/tag.js.map +1 -0
  136. package/dist/components/css/textarea/textarea.d.ts +23 -0
  137. package/dist/components/css/textarea/textarea.d.ts.map +1 -0
  138. package/dist/components/css/textarea/textarea.js +18 -0
  139. package/dist/components/css/textarea/textarea.js.map +1 -0
  140. package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
  141. package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
  142. package/dist/components/css/toggle-group/toggle-group.js +29 -0
  143. package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
  144. package/dist/components/css/tooltip/tooltip.d.ts +30 -0
  145. package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
  146. package/dist/components/css/tooltip/tooltip.js +28 -0
  147. package/dist/components/css/tooltip/tooltip.js.map +1 -0
  148. package/dist/components/frittstaende/calendar/fs-calendar.d.ts +52 -0
  149. package/dist/components/frittstaende/calendar/fs-calendar.d.ts.map +1 -0
  150. package/dist/components/frittstaende/calendar/fs-calendar.js +462 -0
  151. package/dist/components/frittstaende/calendar/fs-calendar.js.map +1 -0
  152. package/dist/components/frittstaende/date-field/fs-date-field.d.ts +110 -0
  153. package/dist/components/frittstaende/date-field/fs-date-field.d.ts.map +1 -0
  154. package/dist/components/frittstaende/date-field/fs-date-field.js +264 -0
  155. package/dist/components/frittstaende/date-field/fs-date-field.js.map +1 -0
  156. package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts +133 -0
  157. package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts.map +1 -0
  158. package/dist/components/frittstaende/suggestion/fs-suggestion.js +280 -0
  159. package/dist/components/frittstaende/suggestion/fs-suggestion.js.map +1 -0
  160. package/dist/components/frittstaende/toast/fs-toast.d.ts +67 -0
  161. package/dist/components/frittstaende/toast/fs-toast.d.ts.map +1 -0
  162. package/dist/components/frittstaende/toast/fs-toast.js +108 -0
  163. package/dist/components/frittstaende/toast/fs-toast.js.map +1 -0
  164. package/dist/components/ramme/error-summary/fs-error-summary.d.ts +70 -0
  165. package/dist/components/ramme/error-summary/fs-error-summary.d.ts.map +1 -0
  166. package/dist/components/ramme/error-summary/fs-error-summary.js +155 -0
  167. package/dist/components/ramme/error-summary/fs-error-summary.js.map +1 -0
  168. package/dist/components/ramme/field/field-core.d.ts +70 -0
  169. package/dist/components/ramme/field/field-core.d.ts.map +1 -0
  170. package/dist/components/ramme/field/field-core.js +60 -0
  171. package/dist/components/ramme/field/field-core.js.map +1 -0
  172. package/dist/components/ramme/field/fs-field.d.ts +49 -0
  173. package/dist/components/ramme/field/fs-field.d.ts.map +1 -0
  174. package/dist/components/ramme/field/fs-field.js +118 -0
  175. package/dist/components/ramme/field/fs-field.js.map +1 -0
  176. package/dist/components/ramme/popover/fs-popover.d.ts +68 -0
  177. package/dist/components/ramme/popover/fs-popover.d.ts.map +1 -0
  178. package/dist/components/ramme/popover/fs-popover.js +167 -0
  179. package/dist/components/ramme/popover/fs-popover.js.map +1 -0
  180. package/dist/components/ramme/tabs/fs-tabs.d.ts +61 -0
  181. package/dist/components/ramme/tabs/fs-tabs.d.ts.map +1 -0
  182. package/dist/components/ramme/tabs/fs-tabs.js +156 -0
  183. package/dist/components/ramme/tabs/fs-tabs.js.map +1 -0
  184. package/dist/dom.d.ts +33 -0
  185. package/dist/dom.d.ts.map +1 -0
  186. package/dist/dom.js +77 -0
  187. package/dist/dom.js.map +1 -0
  188. package/dist/fs.d.ts +199 -0
  189. package/dist/fs.d.ts.map +1 -0
  190. package/dist/fs.js +128 -0
  191. package/dist/fs.js.map +1 -0
  192. package/dist/index.d.ts +84 -0
  193. package/dist/index.d.ts.map +1 -0
  194. package/dist/index.js +48 -0
  195. package/dist/index.js.map +1 -0
  196. package/dist/jsx/react.d.ts +127 -0
  197. package/dist/jsx/react.d.ts.map +1 -0
  198. package/dist/jsx/react.js +2 -0
  199. package/dist/jsx/react.js.map +1 -0
  200. package/dist/react.d.ts +207 -0
  201. package/dist/react.d.ts.map +1 -0
  202. package/dist/react.js +109 -0
  203. package/dist/react.js.map +1 -0
  204. package/dist/tailwind/preset.d.ts +120 -0
  205. package/dist/tailwind/preset.d.ts.map +1 -0
  206. package/dist/tailwind/preset.js +107 -0
  207. package/dist/tailwind/preset.js.map +1 -0
  208. package/dist/takeover.d.ts +79 -0
  209. package/dist/takeover.d.ts.map +1 -0
  210. package/dist/takeover.js +119 -0
  211. package/dist/takeover.js.map +1 -0
  212. package/dist/tokens/color.d.ts +85 -0
  213. package/dist/tokens/color.d.ts.map +1 -0
  214. package/dist/tokens/color.js +212 -0
  215. package/dist/tokens/color.js.map +1 -0
  216. package/dist/tokens/theme.d.ts +52 -0
  217. package/dist/tokens/theme.d.ts.map +1 -0
  218. package/dist/tokens/theme.js +197 -0
  219. package/dist/tokens/theme.js.map +1 -0
  220. package/dist/tokens/tokens.d.ts +210 -0
  221. package/dist/tokens/tokens.d.ts.map +1 -0
  222. package/dist/tokens/tokens.js +238 -0
  223. package/dist/tokens/tokens.js.map +1 -0
  224. package/package.json +334 -0
  225. package/src/components/css/accordion/accordion.css +74 -0
  226. package/src/components/css/accordion/accordion.ts +50 -0
  227. package/src/components/css/alert/alert.css +50 -0
  228. package/src/components/css/alert/alert.ts +48 -0
  229. package/src/components/css/avatar/avatar.css +64 -0
  230. package/src/components/css/avatar/avatar.ts +57 -0
  231. package/src/components/css/badge/badge.css +42 -0
  232. package/src/components/css/badge/badge.ts +43 -0
  233. package/src/components/css/breadcrumbs/breadcrumbs.css +50 -0
  234. package/src/components/css/breadcrumbs/breadcrumbs.ts +29 -0
  235. package/src/components/css/button/button.css +101 -0
  236. package/src/components/css/button/button.ts +44 -0
  237. package/src/components/css/card/card.css +48 -0
  238. package/src/components/css/card/card.ts +50 -0
  239. package/src/components/css/checkbox/checkbox.css +96 -0
  240. package/src/components/css/checkbox/checkbox.ts +50 -0
  241. package/src/components/css/dialog/dialog.css +50 -0
  242. package/src/components/css/dialog/dialog.ts +36 -0
  243. package/src/components/css/divider/divider.css +18 -0
  244. package/src/components/css/divider/divider.ts +41 -0
  245. package/src/components/css/error-text/error-text.css +21 -0
  246. package/src/components/css/error-text/error-text.ts +37 -0
  247. package/src/components/css/fieldset/fieldset.css +57 -0
  248. package/src/components/css/fieldset/fieldset.ts +87 -0
  249. package/src/components/css/file-upload/file-upload.css +92 -0
  250. package/src/components/css/file-upload/file-upload.ts +65 -0
  251. package/src/components/css/heading/heading.css +33 -0
  252. package/src/components/css/heading/heading.ts +42 -0
  253. package/src/components/css/help-text/help-text.css +24 -0
  254. package/src/components/css/help-text/help-text.ts +42 -0
  255. package/src/components/css/input/input.css +131 -0
  256. package/src/components/css/input/input.ts +76 -0
  257. package/src/components/css/label/label.css +34 -0
  258. package/src/components/css/label/label.ts +51 -0
  259. package/src/components/css/link/link.css +36 -0
  260. package/src/components/css/link/link.ts +29 -0
  261. package/src/components/css/list/list.css +40 -0
  262. package/src/components/css/list/list.ts +42 -0
  263. package/src/components/css/pagination/pagination.css +77 -0
  264. package/src/components/css/pagination/pagination.ts +33 -0
  265. package/src/components/css/paragraph/paragraph.css +31 -0
  266. package/src/components/css/paragraph/paragraph.ts +53 -0
  267. package/src/components/css/radio/radio.css +76 -0
  268. package/src/components/css/radio/radio.ts +50 -0
  269. package/src/components/css/search/search.css +49 -0
  270. package/src/components/css/search/search.ts +52 -0
  271. package/src/components/css/select/select.css +95 -0
  272. package/src/components/css/select/select.ts +40 -0
  273. package/src/components/css/shared.ts +40 -0
  274. package/src/components/css/skeleton/skeleton.css +49 -0
  275. package/src/components/css/skeleton/skeleton.ts +43 -0
  276. package/src/components/css/skip-link/skip-link.css +36 -0
  277. package/src/components/css/skip-link/skip-link.ts +22 -0
  278. package/src/components/css/spinner/spinner.css +48 -0
  279. package/src/components/css/spinner/spinner.ts +48 -0
  280. package/src/components/css/sr-only/sr-only.css +16 -0
  281. package/src/components/css/sr-only/sr-only.ts +18 -0
  282. package/src/components/css/switch/switch.css +101 -0
  283. package/src/components/css/switch/switch.ts +46 -0
  284. package/src/components/css/table/table.css +63 -0
  285. package/src/components/css/table/table.ts +50 -0
  286. package/src/components/css/tag/tag.css +58 -0
  287. package/src/components/css/tag/tag.ts +49 -0
  288. package/src/components/css/textarea/textarea.css +71 -0
  289. package/src/components/css/textarea/textarea.ts +40 -0
  290. package/src/components/css/toggle-group/toggle-group.css +82 -0
  291. package/src/components/css/toggle-group/toggle-group.ts +37 -0
  292. package/src/components/css/tooltip/tooltip.css +47 -0
  293. package/src/components/css/tooltip/tooltip.ts +36 -0
  294. package/src/components/frittstaende/calendar/fs-calendar.ts +525 -0
  295. package/src/components/frittstaende/date-field/date-field.css +54 -0
  296. package/src/components/frittstaende/date-field/fs-date-field.ts +309 -0
  297. package/src/components/frittstaende/suggestion/fs-suggestion.ts +333 -0
  298. package/src/components/frittstaende/suggestion/suggestion.css +63 -0
  299. package/src/components/frittstaende/toast/fs-toast.ts +146 -0
  300. package/src/components/frittstaende/toast/toast.css +71 -0
  301. package/src/components/ramme/error-summary/error-summary.css +45 -0
  302. package/src/components/ramme/error-summary/fs-error-summary.ts +197 -0
  303. package/src/components/ramme/field/field-core.ts +132 -0
  304. package/src/components/ramme/field/field.css +6 -0
  305. package/src/components/ramme/field/fs-field.ts +154 -0
  306. package/src/components/ramme/popover/fs-popover.ts +205 -0
  307. package/src/components/ramme/popover/popover.css +34 -0
  308. package/src/components/ramme/tabs/fs-tabs.ts +182 -0
  309. package/src/components/ramme/tabs/tabs.css +68 -0
  310. package/src/tailwind/tailwind.css +134 -0
  311. package/src/tokens/color.ts +267 -0
  312. package/src/tokens/theme.ts +350 -0
  313. package/src/tokens/tokens.css +214 -0
  314. package/src/tokens/tokens.ts +293 -0
@@ -0,0 +1,146 @@
1
+ import { html, LitElement } from "lit"
2
+
3
+ export const FS_TOAST_TAG = "fs-toast" as const
4
+
5
+ export const TOAST_CLASS = "fs-toast" as const
6
+ export const TOAST_CLOSE_CLASS = "fs-toast__close" as const
7
+
8
+ export const toastColors = ["neutral", "success", "warning", "danger"] as const
9
+ export type ToastColor = (typeof toastColors)[number]
10
+
11
+ export type ToastOptions = {
12
+ /** Hva meldingen betyr. Standard: `neutral`. */
13
+ color?: ToastColor
14
+ /** Millisekunder før meldingen forsvinner. `0` lar den bli stående. */
15
+ duration?: number
16
+ /** Tekst på lukkeknappen. Standard: «Lukk melding». */
17
+ closeLabel?: string
18
+ }
19
+
20
+ /**
21
+ * Køen av korte meldinger i hjørnet av skjermen.
22
+ *
23
+ * Elementet er beholderen, ikke meldingen. Du legger det inn én gang i
24
+ * appen og kaller `show()` når noe skal meldes.
25
+ *
26
+ * Beholderen er en `role="status"`-region, ikke `role="alert"`. En melding
27
+ * som dukker opp i hjørnet skal ikke avbryte det skjermleseren holder på med;
28
+ * er beskjeden så viktig at den må avbryte, hører den hjemme i en
29
+ * [Alert](../../css/alert/alert.js) i selve siden.
30
+ *
31
+ * Meldinger som forsvinner av seg selv er en tilgjengelighetsfelle: den som
32
+ * leser sakte eller bruker forstørrelse rekker ikke lese dem. Derfor er
33
+ * lukkeknappen alltid der, og tiden stopper mens musa eller fokus er i
34
+ * meldingen.
35
+ *
36
+ * ```ts
37
+ * document.querySelector("fs-toast").show("Søknaden er sendt", {
38
+ * color: "success",
39
+ * })
40
+ * ```
41
+ */
42
+ export class FsToast extends LitElement {
43
+ static properties = {
44
+ duration: { type: Number },
45
+ label: { type: String },
46
+ }
47
+
48
+ /** Standard levetid i millisekunder. `0` lar meldingene bli stående. */
49
+ duration = 6000
50
+ /** Tekst som sier hva regionen er. Blir `aria-label`. */
51
+ label = "Meldinger"
52
+
53
+ createRenderRoot() {
54
+ return this
55
+ }
56
+
57
+ render() {
58
+ return html`<slot></slot>`
59
+ }
60
+
61
+ connectedCallback() {
62
+ super.connectedCallback()
63
+ this.setAttribute("role", "status")
64
+ this.setAttribute("aria-live", "polite")
65
+ this.setAttribute("aria-label", this.label)
66
+ }
67
+
68
+ updated() {
69
+ // `label` kan settes etter at elementet står i DOM-en, for eksempel av et
70
+ // rammeverk som fyller inn attributtene i et senere steg.
71
+ this.setAttribute("aria-label", this.label)
72
+ }
73
+
74
+ /** Viser en melding, og returnerer elementet den ble lagt i. */
75
+ show(message: string, options: ToastOptions = {}): HTMLElement {
76
+ const {
77
+ color = "neutral",
78
+ duration = this.duration,
79
+ closeLabel = "Lukk melding",
80
+ } = options
81
+
82
+ const toast = document.createElement("div")
83
+ toast.className = TOAST_CLASS
84
+ if (color !== "neutral") toast.dataset.color = color
85
+
86
+ const text = document.createElement("span")
87
+ text.textContent = message
88
+ toast.append(text)
89
+
90
+ const close = document.createElement("button")
91
+ close.type = "button"
92
+ close.className = TOAST_CLOSE_CLASS
93
+ close.setAttribute("aria-label", closeLabel)
94
+ close.textContent = "×"
95
+ close.addEventListener("click", () => this.dismiss(toast))
96
+ toast.append(close)
97
+
98
+ this.prepend(toast)
99
+
100
+ if (duration > 0) {
101
+ let timer = window.setTimeout(() => this.dismiss(toast), duration)
102
+
103
+ // Tiden stopper mens brukeren leser eller er på vei til lukkeknappen.
104
+ const pause = () => window.clearTimeout(timer)
105
+ const resume = () => {
106
+ timer = window.setTimeout(() => this.dismiss(toast), duration)
107
+ }
108
+
109
+ toast.addEventListener("mouseenter", pause)
110
+ toast.addEventListener("mouseleave", resume)
111
+ toast.addEventListener("focusin", pause)
112
+ toast.addEventListener("focusout", resume)
113
+ }
114
+
115
+ return toast
116
+ }
117
+
118
+ /** Fjerner en melding. */
119
+ dismiss(toast: HTMLElement) {
120
+ if (!this.contains(toast)) return
121
+ toast.remove()
122
+
123
+ this.dispatchEvent(
124
+ new CustomEvent("toast-dismiss", { bubbles: true, composed: true }),
125
+ )
126
+ }
127
+
128
+ /** Fjerner alle meldingene. */
129
+ clear() {
130
+ for (const toast of this.querySelectorAll(`.${TOAST_CLASS}`)) {
131
+ toast.remove()
132
+ }
133
+ }
134
+ }
135
+
136
+ declare global {
137
+ interface HTMLElementTagNameMap {
138
+ "fs-toast": FsToast
139
+ }
140
+ }
141
+
142
+ export function defineFsToast(tagName = FS_TOAST_TAG): void {
143
+ if (!customElements.get(tagName)) {
144
+ customElements.define(tagName, FsToast)
145
+ }
146
+ }
@@ -0,0 +1,71 @@
1
+ @layer fristil {
2
+ fs-toast {
3
+ /* Beholderen ligger fast i hjørnet og slipper klikk gjennom, så den ikke
4
+ stenger for knapper under seg når den er tom. */
5
+ position: fixed;
6
+ inset-block-end: var(--fs-toast-space, var(--size-4));
7
+ inset-inline-end: var(--fs-toast-space, var(--size-4));
8
+ z-index: 60;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--size-2);
12
+ inline-size: var(--fs-toast-width, min(24rem, calc(100vw - var(--size-8))));
13
+ pointer-events: none;
14
+ }
15
+
16
+ .fs-toast {
17
+ box-sizing: border-box;
18
+ display: flex;
19
+ align-items: flex-start;
20
+ justify-content: space-between;
21
+ gap: var(--size-3);
22
+ padding: var(--fs-toast-padding, var(--size-3) var(--size-4));
23
+ font-family: inherit;
24
+ font-size: var(--font-size-m);
25
+ line-height: 1.5;
26
+ color: var(--semantic-page-foreground);
27
+ background: var(--semantic-page-background);
28
+ border: 1px solid var(--semantic-divider-30);
29
+ border-inline-start: var(--size-1) solid var(--semantic-neutral-foreground);
30
+ border-radius: var(--fs-toast-radius, var(--size-1));
31
+ box-shadow: var(--semantic-shadow-overlay);
32
+ pointer-events: auto;
33
+ }
34
+
35
+ .fs-toast[data-color="success"] {
36
+ border-inline-start-color: var(--semantic-success-foreground);
37
+ }
38
+
39
+ .fs-toast[data-color="warning"] {
40
+ border-inline-start-color: var(--semantic-warning-foreground);
41
+ }
42
+
43
+ .fs-toast[data-color="danger"] {
44
+ border-inline-start-color: var(--semantic-danger-foreground);
45
+ }
46
+
47
+ .fs-toast__close {
48
+ flex: none;
49
+ min-inline-size: var(--size-6);
50
+ min-block-size: var(--size-6);
51
+ padding: 0;
52
+ font-family: inherit;
53
+ font-size: var(--font-size-l);
54
+ line-height: 1;
55
+ color: var(--semantic-muted-foreground);
56
+ background: transparent;
57
+ border: 0;
58
+ border-radius: var(--size-1);
59
+ cursor: pointer;
60
+ }
61
+
62
+ .fs-toast__close:hover {
63
+ color: var(--semantic-page-foreground);
64
+ background: var(--semantic-neutral-background);
65
+ }
66
+
67
+ .fs-toast__close:focus-visible {
68
+ outline: 2px solid var(--semantic-interactive-main);
69
+ outline-offset: 2px;
70
+ }
71
+ }
@@ -0,0 +1,45 @@
1
+ @import "../../css/list/list.css";
2
+
3
+ @layer fristil {
4
+ fs-error-summary[hidden] {
5
+ display: none;
6
+ }
7
+
8
+ /* Klassen settes på selve elementet av komponenten. Konsumentens markup
9
+ blir stående der den er. */
10
+ .fs-error-summary {
11
+ display: block;
12
+ box-sizing: border-box;
13
+ padding: var(--fs-error-summary-padding, var(--size-4));
14
+ font-family: inherit;
15
+ font-size: var(--font-size-m);
16
+ line-height: 1.5;
17
+ color: var(--semantic-page-foreground);
18
+ background: var(--semantic-danger-background);
19
+ border-inline-start: var(--size-1) solid var(--semantic-danger-foreground);
20
+ border-radius: var(--fs-error-summary-radius, var(--size-1));
21
+ }
22
+
23
+ .fs-error-summary:focus-visible {
24
+ outline: 2px solid var(--semantic-danger-foreground);
25
+ outline-offset: 2px;
26
+ }
27
+
28
+ .fs-error-summary__title {
29
+ margin: 0 0 var(--size-2);
30
+ font-size: var(--font-size-l);
31
+ font-weight: 600;
32
+ line-height: 1.4;
33
+ }
34
+
35
+ .fs-error-summary a {
36
+ color: var(--semantic-danger-foreground);
37
+ text-decoration: underline;
38
+ text-underline-offset: 0.14em;
39
+ }
40
+
41
+ .fs-error-summary a:focus-visible {
42
+ outline: 2px solid var(--semantic-danger-foreground);
43
+ outline-offset: 2px;
44
+ }
45
+ }
@@ -0,0 +1,197 @@
1
+ import { html, LitElement } from "lit"
2
+
3
+ export const FS_ERROR_SUMMARY_TAG = "fs-error-summary" as const
4
+
5
+ export const ERROR_SUMMARY_CLASS = "fs-error-summary" as const
6
+ export const ERROR_SUMMARY_TITLE_CLASS = "fs-error-summary__title" as const
7
+
8
+ /**
9
+ * Samler feilene i et skjema og sender brukeren til feltet som feilet.
10
+ *
11
+ * Du skriver lista med feil selv, som lenker til id-en på hvert felt.
12
+ * Komponenten gjør tre ting som er lette å glemme, og som avgjør om
13
+ * oppsummeringen er til nytte:
14
+ *
15
+ * 1. Flytter fokus til boksen når den kommer til syne, så den som hører siden
16
+ * får vite at innsendingen stoppet.
17
+ * 2. Gir fokus til selve kontrollen når en lenke følges. En vanlig ankerlenke
18
+ * ruller bare dit, og neste tastetrykk fortsetter der fokus sto før.
19
+ * 3. Holder boksen skjult mens lista er tom, så en tom overskrift ikke blir
20
+ * stående i skjemaet.
21
+ *
22
+ * ```html
23
+ * <fs-error-summary heading="Skjemaet har to feil">
24
+ * <ul class="fs-list">
25
+ * <li><a href="#epost">Skriv en gyldig e-postadresse</a></li>
26
+ * </ul>
27
+ * </fs-error-summary>
28
+ * ```
29
+ */
30
+ export class FsErrorSummary extends LitElement {
31
+ static properties = {
32
+ heading: { type: String },
33
+ autofocus: { type: Boolean, reflect: true },
34
+ }
35
+
36
+ /** Overskriften over lista. */
37
+ heading = "Skjemaet har feil"
38
+ /** Flytt fokus hit når boksen kommer til syne. Standard: på. */
39
+ autofocus = true
40
+
41
+ private hasFocused = false
42
+ /** Tittelen komponenten selv har laget, til forskjell fra konsumentens. */
43
+ private ownTitle?: HTMLElement
44
+
45
+ createRenderRoot() {
46
+ return this
47
+ }
48
+
49
+ render() {
50
+ return html`<slot @slotchange=${this.handleSlotChange}></slot>`
51
+ }
52
+
53
+ /**
54
+ * Ser etter at lista med feil endrer seg.
55
+ *
56
+ * Komponenten rendrer i vanlig DOM, og da melder ikke `slotchange` fra når
57
+ * konsumenten bytter ut innholdet. Feilene kommer og går mens brukeren
58
+ * fyller ut skjemaet, så endringen må observeres.
59
+ */
60
+ private observer?: MutationObserver
61
+ private syncing = false
62
+
63
+ connectedCallback() {
64
+ super.connectedCallback()
65
+ this.observer = new MutationObserver(() => {
66
+ if (!this.syncing) this.sync()
67
+ })
68
+ this.observer.observe(this, { childList: true, subtree: true })
69
+ }
70
+
71
+ disconnectedCallback() {
72
+ this.observer?.disconnect()
73
+ this.observer = undefined
74
+ super.disconnectedCallback()
75
+ }
76
+
77
+ firstUpdated() {
78
+ this.sync()
79
+ }
80
+
81
+ updated() {
82
+ this.sync()
83
+ }
84
+
85
+ private handleSlotChange = () => {
86
+ this.sync()
87
+ }
88
+
89
+ private get items(): HTMLAnchorElement[] {
90
+ return [...this.querySelectorAll<HTMLAnchorElement>("li a[href^='#']")]
91
+ }
92
+
93
+ private sync() {
94
+ if (this.syncing) return
95
+ this.syncing = true
96
+
97
+ try {
98
+ const links = this.items
99
+ const empty = links.length === 0
100
+
101
+ this.hidden = empty
102
+ if (empty) {
103
+ this.hasFocused = false
104
+ return
105
+ }
106
+
107
+ // Boksen er selve elementet. Vi flytter ikke konsumentens markup inn i
108
+ // et omslag: da ville Lit-delen vår og innholdet byttet plass i DOM-en.
109
+ this.classList.add(ERROR_SUMMARY_CLASS)
110
+ this.setAttribute("role", "alert")
111
+ this.tabIndex = -1
112
+
113
+ let title = this.querySelector<HTMLElement>(
114
+ `.${ERROR_SUMMARY_TITLE_CLASS}`,
115
+ )
116
+ if (!title) {
117
+ title = document.createElement("h2")
118
+ title.className = ERROR_SUMMARY_TITLE_CLASS
119
+ this.prepend(title)
120
+ this.ownTitle = title
121
+ }
122
+
123
+ // Overskriften følger `heading` så lenge det er vi som har laget den.
124
+ // Antallet feil endrer seg mens brukeren retter, og en overskrift som
125
+ // ble stående på det første tallet er verre enn ingen: den som hører
126
+ // siden får vite at det er to feil igjen når det er én.
127
+ // Har konsumenten skrevet tittelen selv, er den deres.
128
+ //
129
+ // Sammenligningen er ikke pynt: å skrive den samme teksten på nytt
130
+ // teller som en endring for MutationObserver, og da kaller den sync()
131
+ // igjen i det uendelige.
132
+ const skalSkrive = title === this.ownTitle || !title.textContent?.trim()
133
+ if (skalSkrive && title.textContent !== this.heading) {
134
+ title.textContent = this.heading
135
+ }
136
+
137
+ for (const link of links) {
138
+ link.removeEventListener("click", this.handleLinkClick)
139
+ link.addEventListener("click", this.handleLinkClick)
140
+ }
141
+
142
+ if (this.autofocus && !this.hasFocused) {
143
+ this.hasFocused = true
144
+ this.focus()
145
+ }
146
+ } finally {
147
+ this.syncing = false
148
+ }
149
+ }
150
+
151
+ private handleLinkClick = (event: Event) => {
152
+ const link = event.currentTarget as HTMLAnchorElement
153
+ const id = link.getAttribute("href")?.slice(1)
154
+ if (!id) return
155
+
156
+ const target = document.getElementById(id)
157
+ if (!target) return
158
+
159
+ event.preventDefault()
160
+
161
+ // Er lenken til en ledetekst, skal fokus til kontrollen den peker på.
162
+ const control =
163
+ target instanceof HTMLLabelElement && target.htmlFor
164
+ ? document.getElementById(target.htmlFor)
165
+ : target
166
+
167
+ const focusable = control ?? target
168
+ if (!focusable.hasAttribute("tabindex") && !isFocusable(focusable)) {
169
+ focusable.setAttribute("tabindex", "-1")
170
+ }
171
+
172
+ focusable.focus()
173
+ focusable.scrollIntoView({ block: "center", behavior: "smooth" })
174
+ }
175
+ }
176
+
177
+ function isFocusable(element: Element): boolean {
178
+ return (
179
+ element instanceof HTMLInputElement ||
180
+ element instanceof HTMLSelectElement ||
181
+ element instanceof HTMLTextAreaElement ||
182
+ element instanceof HTMLButtonElement ||
183
+ element instanceof HTMLAnchorElement
184
+ )
185
+ }
186
+
187
+ declare global {
188
+ interface HTMLElementTagNameMap {
189
+ "fs-error-summary": FsErrorSummary
190
+ }
191
+ }
192
+
193
+ export function defineFsErrorSummary(tagName = FS_ERROR_SUMMARY_TAG): void {
194
+ if (!customElements.get(tagName)) {
195
+ customElements.define(tagName, FsErrorSummary)
196
+ }
197
+ }
@@ -0,0 +1,132 @@
1
+ import type { FieldState, RequiredMarker } from "../../css/shared.js"
2
+ import { attributes } from "../../css/shared.js"
3
+
4
+ /**
5
+ * Regner ut koblingen mellom ledetekst, felt, hjelpetekst og feilmelding.
6
+ *
7
+ * Dette er den ene implementasjonen av tilgjengelighetskontrakten i systemet.
8
+ * `<fs-field>` bruker den på elementer som allerede står i DOM-en, og
9
+ * `fs.field()` gir den samme utregningen som data til den som vil eie
10
+ * markupen selv. Uten delingen ville kontrakten finnes to steder og kunne gå
11
+ * fra hverandre.
12
+ */
13
+
14
+ export type FieldOptions = {
15
+ /** Id på kontrollen. Lages automatisk hvis den utelates. */
16
+ id?: string
17
+ /** Feltet har en hjelpetekst som skal kobles med `aria-describedby`. */
18
+ help?: boolean
19
+ /** Feltet har en feilmelding. Den skjules til feltet er ugyldig. */
20
+ error?: boolean
21
+ /** Egen id på hjelpeteksten. Utledes fra `id` hvis den utelates. */
22
+ helpId?: string
23
+ /** Egen id på feilmeldingen. Utledes fra `id` hvis den utelates. */
24
+ errorId?: string
25
+ /** Marker ledeteksten som påkrevd. */
26
+ required?: RequiredMarker
27
+ /** Marker ledeteksten som valgfri. */
28
+ optional?: boolean
29
+ /** Feltet er ugyldig: viser feilmeldingen og setter `aria-invalid`. */
30
+ invalid?: boolean
31
+ /** Kontrollen er slått av. */
32
+ disabled?: boolean
33
+ /** Ekstra id-er som skal med i `aria-describedby`. */
34
+ describedBy?: string[]
35
+ }
36
+
37
+ export type FieldAttributes = {
38
+ label: {
39
+ class: "fs-label"
40
+ for: string
41
+ "data-required"?: RequiredMarker
42
+ "data-optional"?: ""
43
+ "aria-disabled"?: "true"
44
+ }
45
+ control: {
46
+ id: string
47
+ "aria-describedby"?: string
48
+ "aria-invalid"?: "true"
49
+ "data-state"?: "invalid"
50
+ disabled?: true
51
+ }
52
+ help: { id: string }
53
+ error: { id: string; hidden?: true }
54
+ /** Tilstanden kontrollen skal ha, til `input()`, `textarea()` og `select()`. */
55
+ state: FieldState
56
+ }
57
+
58
+ let counter = 0
59
+
60
+ /** Lager en id som er unik innenfor dokumentet. */
61
+ export function createFieldId(): string {
62
+ counter += 1
63
+ return `fs-field-${counter}-${Math.random().toString(36).slice(2, 8)}`
64
+ }
65
+
66
+ /**
67
+ * Setter sammen `aria-describedby` av hjelpetekst, feilmelding og det
68
+ * kalleren har lagt til selv.
69
+ *
70
+ * Feilmeldingen tas bare med når feltet faktisk er ugyldig. Ellers ville
71
+ * `aria-describedby` pekt på et skjult element, og skjermlesere melder da
72
+ * enten ingenting eller noe som ikke står på skjermen.
73
+ */
74
+ export function joinDescribedBy(
75
+ ids: Array<string | undefined | false>,
76
+ ): string | undefined {
77
+ const unique = new Set<string>()
78
+ for (const id of ids) {
79
+ if (!id) continue
80
+ for (const part of id.split(/\s+/)) {
81
+ if (part) unique.add(part)
82
+ }
83
+ }
84
+ return unique.size > 0 ? [...unique].join(" ") : undefined
85
+ }
86
+
87
+ export function computeFieldAttributes(
88
+ options: FieldOptions = {},
89
+ ): FieldAttributes {
90
+ const {
91
+ id = createFieldId(),
92
+ help = false,
93
+ error = false,
94
+ required,
95
+ optional,
96
+ invalid = false,
97
+ disabled = false,
98
+ describedBy = [],
99
+ helpId = `${id}-help`,
100
+ errorId = `${id}-error`,
101
+ } = options
102
+
103
+ return {
104
+ label: attributes({
105
+ class: "fs-label" as const,
106
+ for: id,
107
+ "data-required": required,
108
+ "data-optional": optional && !required ? ("" as const) : undefined,
109
+ "aria-disabled": disabled ? ("true" as const) : undefined,
110
+ }),
111
+ control: attributes({
112
+ id,
113
+ "aria-describedby": joinDescribedBy([
114
+ help && helpId,
115
+ invalid && error && errorId,
116
+ ...describedBy,
117
+ ]),
118
+ "aria-invalid": invalid ? ("true" as const) : undefined,
119
+ // Fargen følger med her, ikke fra input()/textarea()/select(). Ellers
120
+ // ville begge satt aria-invalid, og i maler som skriver ut attributtene
121
+ // bokstavelig, som Astro og ren HTML, ble de stående dobbelt.
122
+ "data-state": invalid ? ("invalid" as const) : undefined,
123
+ disabled: disabled ? (true as const) : undefined,
124
+ }),
125
+ help: { id: helpId },
126
+ error: attributes({
127
+ id: errorId,
128
+ hidden: invalid ? undefined : (true as const),
129
+ }),
130
+ state: invalid ? "invalid" : "default",
131
+ }
132
+ }
@@ -0,0 +1,6 @@
1
+ @import "../../css/label/label.css";
2
+ @import "../../css/input/input.css";
3
+ @import "../../css/textarea/textarea.css";
4
+ @import "../../css/select/select.css";
5
+ @import "../../css/help-text/help-text.css";
6
+ @import "../../css/error-text/error-text.css";