@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,154 @@
1
+ import { html, LitElement } from "lit"
2
+
3
+ import { computeFieldAttributes } from "./field-core.js"
4
+
5
+ export const FS_FIELD_TAG = "fs-field" as const
6
+
7
+ function uniqueId(prefix: string): string {
8
+ return `${prefix}-${Math.random().toString(36).slice(2, 9)}`
9
+ }
10
+
11
+ export class FsField extends LitElement {
12
+ static properties = {
13
+ invalid: { type: Boolean, reflect: true },
14
+ disabled: { type: Boolean, reflect: true },
15
+ optional: { type: Boolean, reflect: true },
16
+ requiredMarker: { type: String, attribute: "required-marker" },
17
+ controlId: { type: String, attribute: "control-id" },
18
+ describedBy: { type: String, attribute: "described-by" },
19
+ }
20
+
21
+ invalid = false
22
+ disabled = false
23
+ optional = false
24
+ requiredMarker: "none" | "symbol" | "text" = "none"
25
+ controlId?: string
26
+ describedBy?: string
27
+
28
+ createRenderRoot() {
29
+ return this
30
+ }
31
+
32
+ render() {
33
+ return html`<slot @slotchange=${this.handleSlotChange}></slot>`
34
+ }
35
+
36
+ firstUpdated() {
37
+ this.syncA11y()
38
+ }
39
+
40
+ updated() {
41
+ this.syncA11y()
42
+ }
43
+
44
+ private handleSlotChange = () => {
45
+ this.syncA11y()
46
+ }
47
+
48
+ private syncA11y() {
49
+ const label = this.querySelector("label") as HTMLLabelElement | null
50
+ const control = this.querySelector(
51
+ "input:not([type='hidden']), textarea, select",
52
+ ) as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null
53
+ const help = this.querySelector(
54
+ ".fs-help-text, [data-role='help']",
55
+ ) as HTMLElement | null
56
+ const error = this.querySelector(
57
+ ".fs-error-text, [data-role='error']",
58
+ ) as HTMLElement | null
59
+
60
+ if (!control) return
61
+
62
+ if (help && !help.id) help.id = uniqueId("fs-field-help")
63
+ if (error && !error.id) error.id = uniqueId("fs-field-error")
64
+
65
+ // Selve kontrakten regnes ut av den delte kjernen, som fs.field() også
66
+ // bruker. Denne komponenten gjør bare én ting utover det: å sette
67
+ // resultatet på elementer som allerede står i DOM-en.
68
+ const computed = computeFieldAttributes({
69
+ id: this.controlId || control.id || uniqueId("fs-field-control"),
70
+ help: Boolean(help),
71
+ error: Boolean(error),
72
+ helpId: help?.id,
73
+ errorId: error?.id,
74
+ required:
75
+ this.requiredMarker === "symbol" || this.requiredMarker === "text"
76
+ ? this.requiredMarker
77
+ : undefined,
78
+ optional: this.optional,
79
+ invalid: this.invalid,
80
+ disabled: this.disabled,
81
+ describedBy: [
82
+ control.getAttribute("aria-describedby") ?? "",
83
+ this.describedBy ?? "",
84
+ ].filter(Boolean),
85
+ })
86
+
87
+ control.id = computed.control.id
88
+
89
+ if (label) {
90
+ label.classList.add(computed.label.class)
91
+ if (!label.htmlFor) label.htmlFor = computed.label.for
92
+ setOrRemove(label, "data-required", computed.label["data-required"])
93
+ setOrRemove(label, "data-optional", computed.label["data-optional"])
94
+ setOrRemove(label, "aria-disabled", computed.label["aria-disabled"])
95
+ }
96
+
97
+ if (error) {
98
+ error.hidden = Boolean(computed.error.hidden)
99
+ error.setAttribute("aria-hidden", String(Boolean(computed.error.hidden)))
100
+ }
101
+
102
+ setOrRemove(
103
+ control,
104
+ "aria-describedby",
105
+ computed.control["aria-describedby"],
106
+ )
107
+ setOrRemove(control, "aria-invalid", computed.control["aria-invalid"])
108
+
109
+ if (this.disabled) {
110
+ control.setAttribute("disabled", "")
111
+ control.setAttribute("aria-disabled", "true")
112
+ } else {
113
+ control.removeAttribute("disabled")
114
+ control.removeAttribute("aria-disabled")
115
+ }
116
+
117
+ // data-state settes bare når konsumenten ikke har satt den selv.
118
+ const isSystemField =
119
+ control.classList.contains("fs-input") ||
120
+ control.classList.contains("fs-textarea") ||
121
+ control.classList.contains("fs-select")
122
+
123
+ const state = computed.control["data-state"]
124
+ if (state && isSystemField && !control.hasAttribute("data-state")) {
125
+ control.setAttribute("data-state", state)
126
+ } else if (!state && control.getAttribute("data-state") === "invalid") {
127
+ control.removeAttribute("data-state")
128
+ }
129
+ }
130
+ }
131
+
132
+ function setOrRemove(
133
+ element: HTMLElement,
134
+ name: string,
135
+ value: string | undefined,
136
+ ) {
137
+ if (value === undefined) {
138
+ element.removeAttribute(name)
139
+ } else {
140
+ element.setAttribute(name, value)
141
+ }
142
+ }
143
+
144
+ declare global {
145
+ interface HTMLElementTagNameMap {
146
+ "fs-field": FsField
147
+ }
148
+ }
149
+
150
+ export function defineFsField(tagName = FS_FIELD_TAG): void {
151
+ if (!customElements.get(tagName)) {
152
+ customElements.define(tagName, FsField)
153
+ }
154
+ }
@@ -0,0 +1,205 @@
1
+ import { html, LitElement } from "lit"
2
+
3
+ export const FS_POPOVER_TAG = "fs-popover" as const
4
+
5
+ export const POPOVER_CLASS = "fs-popover" as const
6
+
7
+ type Placement = "bottom-start" | "bottom-end" | "top-start" | "top-end"
8
+
9
+ /**
10
+ * Et panel som henger under en knapp, og som lukker seg selv.
11
+ *
12
+ * Panelet bruker nettleserens egen `popover`, så det havner i topplaget. Det
13
+ * løser tre ting som ellers krever kode: panelet legger seg over alt annet
14
+ * uten `z-index`, Escape lukker det, og et klikk utenfor lukker det.
15
+ *
16
+ * Det topplaget gjør ikke er å plassere panelet. `position-anchor` finnes
17
+ * ennå ikke i alle nettlesere, så posisjonen regnes ut her, mot knappens
18
+ * plass på skjermen.
19
+ *
20
+ * Du skriver knappen og innholdet selv, og komponenten kobler dem sammen:
21
+ *
22
+ * ```html
23
+ * <fs-popover placement="bottom-end">
24
+ * <button slot="trigger" class="fs-button">Handlinger</button>
25
+ * <ul class="fs-list" data-variant="plain">…</ul>
26
+ * </fs-popover>
27
+ * ```
28
+ */
29
+ export class FsPopover extends LitElement {
30
+ static properties = {
31
+ open: { type: Boolean, reflect: true },
32
+ placement: { type: String },
33
+ }
34
+
35
+ /** Om panelet er åpent. Speiles, så CSS kan treffe tilstanden. */
36
+ open = false
37
+ /** Hvilken kant panelet henger fra. Standard: `bottom-start`. */
38
+ placement: Placement = "bottom-start"
39
+
40
+ private panel?: HTMLElement
41
+ private triggerElement?: HTMLElement
42
+
43
+ createRenderRoot() {
44
+ return this
45
+ }
46
+
47
+ render() {
48
+ return html`<slot @slotchange=${this.handleSlotChange}></slot>`
49
+ }
50
+
51
+ connectedCallback() {
52
+ super.connectedCallback()
53
+ window.addEventListener("resize", this.reposition)
54
+ window.addEventListener("scroll", this.reposition, true)
55
+ }
56
+
57
+ disconnectedCallback() {
58
+ window.removeEventListener("resize", this.reposition)
59
+ window.removeEventListener("scroll", this.reposition, true)
60
+ super.disconnectedCallback()
61
+ }
62
+
63
+ firstUpdated() {
64
+ this.sync()
65
+ }
66
+
67
+ updated() {
68
+ this.sync()
69
+ }
70
+
71
+ private handleSlotChange = () => {
72
+ this.sync()
73
+ }
74
+
75
+ private sync() {
76
+ const trigger = this.querySelector<HTMLElement>("[slot='trigger']")
77
+ const panel = [...this.children].find(
78
+ (child): child is HTMLElement =>
79
+ child instanceof HTMLElement &&
80
+ child.getAttribute("slot") !== "trigger" &&
81
+ child.tagName !== "SLOT",
82
+ )
83
+
84
+ if (!trigger || !panel) return
85
+
86
+ this.triggerElement = trigger
87
+ this.panel = panel
88
+
89
+ if (!panel.id) {
90
+ panel.id = `fs-popover-${Math.random().toString(36).slice(2, 9)}`
91
+ }
92
+
93
+ panel.classList.add(POPOVER_CLASS)
94
+ // `manual` og ikke `auto`: vi lukker selv, slik at knappen kan brukes til
95
+ // å lukke igjen uten at nettleseren rekker å lukke først.
96
+ panel.setAttribute("popover", "manual")
97
+
98
+ trigger.setAttribute("aria-expanded", String(this.open))
99
+ trigger.setAttribute("aria-controls", panel.id)
100
+
101
+ trigger.removeEventListener("click", this.handleTriggerClick)
102
+ trigger.addEventListener("click", this.handleTriggerClick)
103
+
104
+ if (this.open) {
105
+ if (!panel.matches(":popover-open")) panel.showPopover()
106
+ this.reposition()
107
+ document.addEventListener("click", this.handleOutsideClick, true)
108
+ document.addEventListener("keydown", this.handleKeydown)
109
+ } else {
110
+ if (panel.matches(":popover-open")) panel.hidePopover()
111
+ document.removeEventListener("click", this.handleOutsideClick, true)
112
+ document.removeEventListener("keydown", this.handleKeydown)
113
+ }
114
+ }
115
+
116
+ private handleTriggerClick = () => {
117
+ this.toggle()
118
+ }
119
+
120
+ private handleOutsideClick = (event: Event) => {
121
+ const target = event.target as Node
122
+ if (this.contains(target) || this.panel?.contains(target)) return
123
+ this.hide()
124
+ }
125
+
126
+ private handleKeydown = (event: KeyboardEvent) => {
127
+ if (event.key !== "Escape") return
128
+ this.hide()
129
+ // Fokus tilbake til knappen. Uten dette står fokus på et panel som ikke
130
+ // lenger finnes, og neste tastetrykk starter på toppen av siden.
131
+ this.triggerElement?.focus()
132
+ }
133
+
134
+ /** Regner ut hvor panelet skal stå, mot knappens plass på skjermen. */
135
+ private reposition = () => {
136
+ if (!this.open || !this.panel || !this.triggerElement) return
137
+
138
+ const anchor = this.triggerElement.getBoundingClientRect()
139
+ const panel = this.panel.getBoundingClientRect()
140
+ const space = 4
141
+
142
+ const below = this.placement.startsWith("bottom")
143
+ const alignEnd = this.placement.endsWith("end")
144
+
145
+ let top = below ? anchor.bottom + space : anchor.top - panel.height - space
146
+ let left = alignEnd ? anchor.right - panel.width : anchor.left
147
+
148
+ // Panelet skal ikke havne utenfor skjermen. Går det ut på siden, flyttes
149
+ // det inn; er det ikke plass under, legges det over knappen i stedet.
150
+ left = Math.max(
151
+ space,
152
+ Math.min(left, window.innerWidth - panel.width - space),
153
+ )
154
+ if (below && top + panel.height > window.innerHeight) {
155
+ top = Math.max(space, anchor.top - panel.height - space)
156
+ }
157
+
158
+ this.panel.style.setProperty("--fs-popover-top", `${Math.round(top)}px`)
159
+ this.panel.style.setProperty("--fs-popover-left", `${Math.round(left)}px`)
160
+ }
161
+
162
+ /** Åpner panelet. */
163
+ show() {
164
+ if (this.open) return
165
+ this.open = true
166
+ this.dispatchEvent(
167
+ new CustomEvent("popover-toggle", {
168
+ detail: { open: true },
169
+ bubbles: true,
170
+ composed: true,
171
+ }),
172
+ )
173
+ }
174
+
175
+ /** Lukker panelet. */
176
+ hide() {
177
+ if (!this.open) return
178
+ this.open = false
179
+ this.dispatchEvent(
180
+ new CustomEvent("popover-toggle", {
181
+ detail: { open: false },
182
+ bubbles: true,
183
+ composed: true,
184
+ }),
185
+ )
186
+ }
187
+
188
+ /** Åpner eller lukker. */
189
+ toggle() {
190
+ if (this.open) this.hide()
191
+ else this.show()
192
+ }
193
+ }
194
+
195
+ declare global {
196
+ interface HTMLElementTagNameMap {
197
+ "fs-popover": FsPopover
198
+ }
199
+ }
200
+
201
+ export function defineFsPopover(tagName = FS_POPOVER_TAG): void {
202
+ if (!customElements.get(tagName)) {
203
+ customElements.define(tagName, FsPopover)
204
+ }
205
+ }
@@ -0,0 +1,34 @@
1
+ @layer fristil {
2
+ fs-popover {
3
+ display: contents;
4
+ }
5
+
6
+ .fs-popover {
7
+ /* Panelet ligger i topplaget, så posisjonen er mot vindusruten. Verdiene
8
+ settes av komponenten, som regner dem ut mot knappen. */
9
+ position: fixed;
10
+ inset: auto;
11
+ inset-block-start: var(--fs-popover-top, 0);
12
+ inset-inline-start: var(--fs-popover-left, 0);
13
+ margin: 0;
14
+ box-sizing: border-box;
15
+ inline-size: max-content;
16
+ max-inline-size: var(
17
+ --fs-popover-width,
18
+ min(20rem, calc(100vw - var(--size-8)))
19
+ );
20
+ padding: var(--fs-popover-padding, var(--size-3));
21
+ font-family: inherit;
22
+ font-size: var(--font-size-m);
23
+ line-height: 1.5;
24
+ color: var(--semantic-page-foreground);
25
+ background: var(--semantic-page-background);
26
+ border: 1px solid var(--semantic-divider-30);
27
+ border-radius: var(--fs-popover-radius, var(--size-1));
28
+ box-shadow: var(--semantic-shadow-overlay);
29
+ }
30
+
31
+ .fs-popover:not(:popover-open) {
32
+ display: none;
33
+ }
34
+ }
@@ -0,0 +1,182 @@
1
+ import { html, LitElement } from "lit"
2
+
3
+ export const FS_TABS_TAG = "fs-tabs" as const
4
+
5
+ export const TABS_LIST_CLASS = "fs-tabs__list" as const
6
+ export const TABS_PANEL_CLASS = "fs-tabs__panel" as const
7
+
8
+ function uniqueId(prefix: string): string {
9
+ return `${prefix}-${Math.random().toString(36).slice(2, 9)}`
10
+ }
11
+
12
+ /**
13
+ * Kobler en rad med knapper til panelene de hører til.
14
+ *
15
+ * Du skriver knappene og panelene selv. Komponenten setter rollene, holder
16
+ * `aria-selected` og `aria-controls` i synk, skjuler panelene som ikke er
17
+ * valgt, og tar piltastene.
18
+ *
19
+ * Tastaturet er hele grunnen til at komponenten finnes. I en faneliste skal
20
+ * Tab hoppe forbi hele raden og inn i panelet, mens piltastene flytter mellom
21
+ * fanene. Et sett knapper uten denne koblingen gir én tabbestopp per fane, og
22
+ * skjermleseren sier «knapp» der den skulle sagt «fane, 2 av 3, valgt».
23
+ *
24
+ * ```html
25
+ * <fs-tabs>
26
+ * <div class="fs-tabs__list">
27
+ * <button>Søknaden</button>
28
+ * <button>Vedlegg</button>
29
+ * </div>
30
+ * <div class="fs-tabs__panel">…</div>
31
+ * <div class="fs-tabs__panel">…</div>
32
+ * </fs-tabs>
33
+ * ```
34
+ */
35
+ export class FsTabs extends LitElement {
36
+ static properties = {
37
+ selected: { type: Number, reflect: true },
38
+ label: { type: String },
39
+ }
40
+
41
+ /** Indeksen på fanen som er valgt. */
42
+ selected = 0
43
+ /** Tekst som sier hva fanene velger mellom. Blir `aria-label` på raden. */
44
+ label?: string
45
+
46
+ createRenderRoot() {
47
+ return this
48
+ }
49
+
50
+ render() {
51
+ return html`<slot @slotchange=${this.handleSlotChange}></slot>`
52
+ }
53
+
54
+ firstUpdated() {
55
+ this.sync()
56
+ }
57
+
58
+ updated() {
59
+ this.sync()
60
+ }
61
+
62
+ private handleSlotChange = () => {
63
+ this.sync()
64
+ }
65
+
66
+ private get tabs(): HTMLButtonElement[] {
67
+ const list = this.querySelector(`.${TABS_LIST_CLASS}`)
68
+ return list ? [...list.querySelectorAll("button")] : []
69
+ }
70
+
71
+ private get panels(): HTMLElement[] {
72
+ return [...this.querySelectorAll<HTMLElement>(`.${TABS_PANEL_CLASS}`)]
73
+ }
74
+
75
+ private sync() {
76
+ const list = this.querySelector(`.${TABS_LIST_CLASS}`)
77
+ const tabs = this.tabs
78
+ const panels = this.panels
79
+
80
+ if (!list || tabs.length === 0) return
81
+
82
+ list.setAttribute("role", "tablist")
83
+ if (this.label) list.setAttribute("aria-label", this.label)
84
+
85
+ // Er `selected` satt utenfor rekkevidde, rettes den her. Ellers ville
86
+ // attributtet sagt noe annet enn det brukeren ser.
87
+ const valid = Math.min(Math.max(this.selected, 0), tabs.length - 1)
88
+ if (valid !== this.selected) this.selected = valid
89
+
90
+ tabs.forEach((tab, index) => {
91
+ const panel = panels[index]
92
+ if (!tab.id) tab.id = uniqueId("fs-tab")
93
+ if (panel && !panel.id) panel.id = uniqueId("fs-tabpanel")
94
+
95
+ const isSelected = index === valid
96
+
97
+ tab.setAttribute("role", "tab")
98
+ tab.setAttribute("type", "button")
99
+ tab.setAttribute("aria-selected", String(isSelected))
100
+ // Roving tabindex: bare den valgte fanen er en tabbestopp, så Tab går
101
+ // fra raden og rett inn i panelet.
102
+ tab.tabIndex = isSelected ? 0 : -1
103
+ if (panel) tab.setAttribute("aria-controls", panel.id)
104
+
105
+ tab.removeEventListener("click", this.handleClick)
106
+ tab.addEventListener("click", this.handleClick)
107
+ tab.removeEventListener("keydown", this.handleKeydown)
108
+ tab.addEventListener("keydown", this.handleKeydown)
109
+
110
+ if (panel) {
111
+ panel.setAttribute("role", "tabpanel")
112
+ panel.setAttribute("aria-labelledby", tab.id)
113
+ panel.hidden = !isSelected
114
+ // Panelet får fokus når det ikke har noe å fokusere på selv, ellers
115
+ // hopper Tab rett forbi innholdet som nettopp ble vist.
116
+ panel.tabIndex = 0
117
+ }
118
+ })
119
+ }
120
+
121
+ private handleClick = (event: Event) => {
122
+ const index = this.tabs.indexOf(event.currentTarget as HTMLButtonElement)
123
+ if (index >= 0) this.select(index)
124
+ }
125
+
126
+ private handleKeydown = (event: KeyboardEvent) => {
127
+ const tabs = this.tabs
128
+ const current = tabs.indexOf(event.currentTarget as HTMLButtonElement)
129
+ if (current < 0) return
130
+
131
+ const steps: Record<string, number> = {
132
+ ArrowRight: 1,
133
+ ArrowLeft: -1,
134
+ ArrowDown: 1,
135
+ ArrowUp: -1,
136
+ }
137
+
138
+ let next: number | undefined
139
+
140
+ if (event.key in steps) {
141
+ next = (current + steps[event.key] + tabs.length) % tabs.length
142
+ } else if (event.key === "Home") {
143
+ next = 0
144
+ } else if (event.key === "End") {
145
+ next = tabs.length - 1
146
+ }
147
+
148
+ if (next === undefined) return
149
+
150
+ event.preventDefault()
151
+ this.select(next)
152
+ tabs[next].focus()
153
+ }
154
+
155
+ /** Velger en fane og melder fra. */
156
+ select(index: number) {
157
+ if (index === this.selected) return
158
+
159
+ this.selected = index
160
+ this.sync()
161
+
162
+ this.dispatchEvent(
163
+ new CustomEvent("tab-select", {
164
+ detail: { index },
165
+ bubbles: true,
166
+ composed: true,
167
+ }),
168
+ )
169
+ }
170
+ }
171
+
172
+ declare global {
173
+ interface HTMLElementTagNameMap {
174
+ "fs-tabs": FsTabs
175
+ }
176
+ }
177
+
178
+ export function defineFsTabs(tagName = FS_TABS_TAG): void {
179
+ if (!customElements.get(tagName)) {
180
+ customElements.define(tagName, FsTabs)
181
+ }
182
+ }
@@ -0,0 +1,68 @@
1
+ @layer fristil {
2
+ fs-tabs {
3
+ display: block;
4
+ }
5
+
6
+ .fs-tabs__list {
7
+ display: flex;
8
+ flex-wrap: wrap;
9
+ gap: var(--fs-tabs-gap, var(--size-1));
10
+ border-block-end: 1px solid var(--semantic-divider-30);
11
+ }
12
+
13
+ .fs-tabs__list button {
14
+ box-sizing: border-box;
15
+ padding: var(--fs-tabs-padding, var(--size-2) var(--size-4));
16
+ min-block-size: var(--size-10);
17
+ font-family: inherit;
18
+ font-size: var(--font-size-m);
19
+ line-height: 1.5;
20
+ color: var(--semantic-interactive-main);
21
+ background: transparent;
22
+ border: 0;
23
+ /* Streken under den valgte fanen tegnes på alle, gjennomsiktig på dem som
24
+ ikke er valgt. Da hopper ikke teksten når brukeren bytter fane. */
25
+ border-block-end: var(--size-0-5) solid transparent;
26
+ margin-block-end: -1px;
27
+ cursor: pointer;
28
+ }
29
+
30
+ .fs-tabs__list button:hover {
31
+ background: var(--semantic-interactive-background);
32
+ }
33
+
34
+ .fs-tabs__list button:focus-visible {
35
+ outline: 2px solid var(--semantic-interactive-main);
36
+ outline-offset: -2px;
37
+ }
38
+
39
+ .fs-tabs__list button[aria-selected="true"] {
40
+ color: var(--semantic-page-foreground);
41
+ font-weight: 600;
42
+ border-block-end-color: var(--semantic-interactive-main);
43
+ }
44
+
45
+ .fs-tabs__list button:disabled {
46
+ color: var(--semantic-disabled-foreground);
47
+ cursor: not-allowed;
48
+ }
49
+
50
+ .fs-tabs__panel {
51
+ padding-block-start: var(--fs-tabs-panel-space, var(--size-4));
52
+ }
53
+
54
+ .fs-tabs__panel:focus-visible {
55
+ outline: 2px solid var(--semantic-interactive-main);
56
+ outline-offset: 2px;
57
+ }
58
+
59
+ /* Høykontrast: streken under den valgte fanen tvinges til samme farge som
60
+ de gjennomsiktige strekene under de andre. `Highlight` slipper gjennom,
61
+ og gir tilbake forskjellen. */
62
+ @media (forced-colors: active) {
63
+ .fs-tabs__list button[aria-selected="true"] {
64
+ border-block-end-color: Highlight;
65
+ color: Highlight;
66
+ }
67
+ }
68
+ }