@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.
- package/CHANGELOG.md +85 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +261 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/css/accordion/accordion.d.ts +37 -0
- package/dist/components/css/accordion/accordion.d.ts.map +1 -0
- package/dist/components/css/accordion/accordion.js +31 -0
- package/dist/components/css/accordion/accordion.js.map +1 -0
- package/dist/components/css/alert/alert.d.ts +35 -0
- package/dist/components/css/alert/alert.d.ts.map +1 -0
- package/dist/components/css/alert/alert.js +29 -0
- package/dist/components/css/alert/alert.js.map +1 -0
- package/dist/components/css/avatar/avatar.d.ts +40 -0
- package/dist/components/css/avatar/avatar.d.ts.map +1 -0
- package/dist/components/css/avatar/avatar.js +30 -0
- package/dist/components/css/avatar/avatar.js.map +1 -0
- package/dist/components/css/badge/badge.d.ts +24 -0
- package/dist/components/css/badge/badge.d.ts.map +1 -0
- package/dist/components/css/badge/badge.js +24 -0
- package/dist/components/css/badge/badge.js.map +1 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.d.ts +25 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.d.ts.map +1 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.js +23 -0
- package/dist/components/css/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/css/button/button.d.ts +26 -0
- package/dist/components/css/button/button.d.ts.map +1 -0
- package/dist/components/css/button/button.js +25 -0
- package/dist/components/css/button/button.js.map +1 -0
- package/dist/components/css/card/card.d.ts +36 -0
- package/dist/components/css/card/card.d.ts.map +1 -0
- package/dist/components/css/card/card.js +28 -0
- package/dist/components/css/card/card.js.map +1 -0
- package/dist/components/css/checkbox/checkbox.d.ts +32 -0
- package/dist/components/css/checkbox/checkbox.d.ts.map +1 -0
- package/dist/components/css/checkbox/checkbox.js +27 -0
- package/dist/components/css/checkbox/checkbox.js.map +1 -0
- package/dist/components/css/dialog/dialog.d.ts +30 -0
- package/dist/components/css/dialog/dialog.d.ts.map +1 -0
- package/dist/components/css/dialog/dialog.js +28 -0
- package/dist/components/css/dialog/dialog.js.map +1 -0
- package/dist/components/css/divider/divider.d.ts +28 -0
- package/dist/components/css/divider/divider.d.ts.map +1 -0
- package/dist/components/css/divider/divider.js +22 -0
- package/dist/components/css/divider/divider.js.map +1 -0
- package/dist/components/css/error-text/error-text.d.ts +24 -0
- package/dist/components/css/error-text/error-text.d.ts.map +1 -0
- package/dist/components/css/error-text/error-text.js +18 -0
- package/dist/components/css/error-text/error-text.js.map +1 -0
- package/dist/components/css/fieldset/fieldset.d.ts +55 -0
- package/dist/components/css/fieldset/fieldset.d.ts.map +1 -0
- package/dist/components/css/fieldset/fieldset.js +39 -0
- package/dist/components/css/fieldset/fieldset.js.map +1 -0
- package/dist/components/css/file-upload/file-upload.d.ts +41 -0
- package/dist/components/css/file-upload/file-upload.d.ts.map +1 -0
- package/dist/components/css/file-upload/file-upload.js +32 -0
- package/dist/components/css/file-upload/file-upload.js.map +1 -0
- package/dist/components/css/heading/heading.d.ts +29 -0
- package/dist/components/css/heading/heading.d.ts.map +1 -0
- package/dist/components/css/heading/heading.js +23 -0
- package/dist/components/css/heading/heading.js.map +1 -0
- package/dist/components/css/help-text/help-text.d.ts +24 -0
- package/dist/components/css/help-text/help-text.d.ts.map +1 -0
- package/dist/components/css/help-text/help-text.js +23 -0
- package/dist/components/css/help-text/help-text.js.map +1 -0
- package/dist/components/css/input/input.d.ts +39 -0
- package/dist/components/css/input/input.d.ts.map +1 -0
- package/dist/components/css/input/input.js +44 -0
- package/dist/components/css/input/input.js.map +1 -0
- package/dist/components/css/label/label.d.ts +33 -0
- package/dist/components/css/label/label.d.ts.map +1 -0
- package/dist/components/css/label/label.js +23 -0
- package/dist/components/css/label/label.js.map +1 -0
- package/dist/components/css/link/link.d.ts +21 -0
- package/dist/components/css/link/link.d.ts.map +1 -0
- package/dist/components/css/link/link.js +14 -0
- package/dist/components/css/link/link.js.map +1 -0
- package/dist/components/css/list/list.d.ts +29 -0
- package/dist/components/css/list/list.d.ts.map +1 -0
- package/dist/components/css/list/list.js +23 -0
- package/dist/components/css/list/list.js.map +1 -0
- package/dist/components/css/pagination/pagination.d.ts +27 -0
- package/dist/components/css/pagination/pagination.d.ts.map +1 -0
- package/dist/components/css/pagination/pagination.js +25 -0
- package/dist/components/css/pagination/pagination.js.map +1 -0
- package/dist/components/css/paragraph/paragraph.d.ts +36 -0
- package/dist/components/css/paragraph/paragraph.d.ts.map +1 -0
- package/dist/components/css/paragraph/paragraph.js +26 -0
- package/dist/components/css/paragraph/paragraph.js.map +1 -0
- package/dist/components/css/radio/radio.d.ts +32 -0
- package/dist/components/css/radio/radio.d.ts.map +1 -0
- package/dist/components/css/radio/radio.js +27 -0
- package/dist/components/css/radio/radio.js.map +1 -0
- package/dist/components/css/search/search.d.ts +33 -0
- package/dist/components/css/search/search.d.ts.map +1 -0
- package/dist/components/css/search/search.js +31 -0
- package/dist/components/css/search/search.js.map +1 -0
- package/dist/components/css/select/select.d.ts +23 -0
- package/dist/components/css/select/select.d.ts.map +1 -0
- package/dist/components/css/select/select.js +18 -0
- package/dist/components/css/select/select.js.map +1 -0
- package/dist/components/css/shared.d.ts +26 -0
- package/dist/components/css/shared.d.ts.map +1 -0
- package/dist/components/css/shared.js +34 -0
- package/dist/components/css/shared.js.map +1 -0
- package/dist/components/css/skeleton/skeleton.d.ts +29 -0
- package/dist/components/css/skeleton/skeleton.d.ts.map +1 -0
- package/dist/components/css/skeleton/skeleton.js +23 -0
- package/dist/components/css/skeleton/skeleton.js.map +1 -0
- package/dist/components/css/skip-link/skip-link.d.ts +18 -0
- package/dist/components/css/skip-link/skip-link.d.ts.map +1 -0
- package/dist/components/css/skip-link/skip-link.js +16 -0
- package/dist/components/css/skip-link/skip-link.js.map +1 -0
- package/dist/components/css/spinner/spinner.d.ts +33 -0
- package/dist/components/css/spinner/spinner.d.ts.map +1 -0
- package/dist/components/css/spinner/spinner.js +25 -0
- package/dist/components/css/spinner/spinner.js.map +1 -0
- package/dist/components/css/sr-only/sr-only.d.ts +17 -0
- package/dist/components/css/sr-only/sr-only.d.ts.map +1 -0
- package/dist/components/css/sr-only/sr-only.js +14 -0
- package/dist/components/css/sr-only/sr-only.js.map +1 -0
- package/dist/components/css/switch/switch.d.ts +33 -0
- package/dist/components/css/switch/switch.d.ts.map +1 -0
- package/dist/components/css/switch/switch.js +29 -0
- package/dist/components/css/switch/switch.js.map +1 -0
- package/dist/components/css/table/table.d.ts +36 -0
- package/dist/components/css/table/table.d.ts.map +1 -0
- package/dist/components/css/table/table.js +28 -0
- package/dist/components/css/table/table.js.map +1 -0
- package/dist/components/css/tag/tag.d.ts +35 -0
- package/dist/components/css/tag/tag.d.ts.map +1 -0
- package/dist/components/css/tag/tag.js +27 -0
- package/dist/components/css/tag/tag.js.map +1 -0
- package/dist/components/css/textarea/textarea.d.ts +23 -0
- package/dist/components/css/textarea/textarea.d.ts.map +1 -0
- package/dist/components/css/textarea/textarea.js +18 -0
- package/dist/components/css/textarea/textarea.js.map +1 -0
- package/dist/components/css/toggle-group/toggle-group.d.ts +31 -0
- package/dist/components/css/toggle-group/toggle-group.d.ts.map +1 -0
- package/dist/components/css/toggle-group/toggle-group.js +29 -0
- package/dist/components/css/toggle-group/toggle-group.js.map +1 -0
- package/dist/components/css/tooltip/tooltip.d.ts +30 -0
- package/dist/components/css/tooltip/tooltip.d.ts.map +1 -0
- package/dist/components/css/tooltip/tooltip.js +28 -0
- package/dist/components/css/tooltip/tooltip.js.map +1 -0
- package/dist/components/frittstaende/calendar/fs-calendar.d.ts +52 -0
- package/dist/components/frittstaende/calendar/fs-calendar.d.ts.map +1 -0
- package/dist/components/frittstaende/calendar/fs-calendar.js +462 -0
- package/dist/components/frittstaende/calendar/fs-calendar.js.map +1 -0
- package/dist/components/frittstaende/date-field/fs-date-field.d.ts +110 -0
- package/dist/components/frittstaende/date-field/fs-date-field.d.ts.map +1 -0
- package/dist/components/frittstaende/date-field/fs-date-field.js +264 -0
- package/dist/components/frittstaende/date-field/fs-date-field.js.map +1 -0
- package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts +133 -0
- package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts.map +1 -0
- package/dist/components/frittstaende/suggestion/fs-suggestion.js +280 -0
- package/dist/components/frittstaende/suggestion/fs-suggestion.js.map +1 -0
- package/dist/components/frittstaende/toast/fs-toast.d.ts +67 -0
- package/dist/components/frittstaende/toast/fs-toast.d.ts.map +1 -0
- package/dist/components/frittstaende/toast/fs-toast.js +108 -0
- package/dist/components/frittstaende/toast/fs-toast.js.map +1 -0
- package/dist/components/ramme/error-summary/fs-error-summary.d.ts +70 -0
- package/dist/components/ramme/error-summary/fs-error-summary.d.ts.map +1 -0
- package/dist/components/ramme/error-summary/fs-error-summary.js +155 -0
- package/dist/components/ramme/error-summary/fs-error-summary.js.map +1 -0
- package/dist/components/ramme/field/field-core.d.ts +70 -0
- package/dist/components/ramme/field/field-core.d.ts.map +1 -0
- package/dist/components/ramme/field/field-core.js +60 -0
- package/dist/components/ramme/field/field-core.js.map +1 -0
- package/dist/components/ramme/field/fs-field.d.ts +49 -0
- package/dist/components/ramme/field/fs-field.d.ts.map +1 -0
- package/dist/components/ramme/field/fs-field.js +118 -0
- package/dist/components/ramme/field/fs-field.js.map +1 -0
- package/dist/components/ramme/popover/fs-popover.d.ts +68 -0
- package/dist/components/ramme/popover/fs-popover.d.ts.map +1 -0
- package/dist/components/ramme/popover/fs-popover.js +167 -0
- package/dist/components/ramme/popover/fs-popover.js.map +1 -0
- package/dist/components/ramme/tabs/fs-tabs.d.ts +61 -0
- package/dist/components/ramme/tabs/fs-tabs.d.ts.map +1 -0
- package/dist/components/ramme/tabs/fs-tabs.js +156 -0
- package/dist/components/ramme/tabs/fs-tabs.js.map +1 -0
- package/dist/dom.d.ts +33 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +77 -0
- package/dist/dom.js.map +1 -0
- package/dist/fs.d.ts +199 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +128 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx/react.d.ts +127 -0
- package/dist/jsx/react.d.ts.map +1 -0
- package/dist/jsx/react.js +2 -0
- package/dist/jsx/react.js.map +1 -0
- package/dist/react.d.ts +207 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +109 -0
- package/dist/react.js.map +1 -0
- package/dist/tailwind/preset.d.ts +120 -0
- package/dist/tailwind/preset.d.ts.map +1 -0
- package/dist/tailwind/preset.js +107 -0
- package/dist/tailwind/preset.js.map +1 -0
- package/dist/takeover.d.ts +79 -0
- package/dist/takeover.d.ts.map +1 -0
- package/dist/takeover.js +119 -0
- package/dist/takeover.js.map +1 -0
- package/dist/tokens/color.d.ts +85 -0
- package/dist/tokens/color.d.ts.map +1 -0
- package/dist/tokens/color.js +212 -0
- package/dist/tokens/color.js.map +1 -0
- package/dist/tokens/theme.d.ts +52 -0
- package/dist/tokens/theme.d.ts.map +1 -0
- package/dist/tokens/theme.js +197 -0
- package/dist/tokens/theme.js.map +1 -0
- package/dist/tokens/tokens.d.ts +210 -0
- package/dist/tokens/tokens.d.ts.map +1 -0
- package/dist/tokens/tokens.js +238 -0
- package/dist/tokens/tokens.js.map +1 -0
- package/package.json +334 -0
- package/src/components/css/accordion/accordion.css +74 -0
- package/src/components/css/accordion/accordion.ts +50 -0
- package/src/components/css/alert/alert.css +50 -0
- package/src/components/css/alert/alert.ts +48 -0
- package/src/components/css/avatar/avatar.css +64 -0
- package/src/components/css/avatar/avatar.ts +57 -0
- package/src/components/css/badge/badge.css +42 -0
- package/src/components/css/badge/badge.ts +43 -0
- package/src/components/css/breadcrumbs/breadcrumbs.css +50 -0
- package/src/components/css/breadcrumbs/breadcrumbs.ts +29 -0
- package/src/components/css/button/button.css +101 -0
- package/src/components/css/button/button.ts +44 -0
- package/src/components/css/card/card.css +48 -0
- package/src/components/css/card/card.ts +50 -0
- package/src/components/css/checkbox/checkbox.css +96 -0
- package/src/components/css/checkbox/checkbox.ts +50 -0
- package/src/components/css/dialog/dialog.css +50 -0
- package/src/components/css/dialog/dialog.ts +36 -0
- package/src/components/css/divider/divider.css +18 -0
- package/src/components/css/divider/divider.ts +41 -0
- package/src/components/css/error-text/error-text.css +21 -0
- package/src/components/css/error-text/error-text.ts +37 -0
- package/src/components/css/fieldset/fieldset.css +57 -0
- package/src/components/css/fieldset/fieldset.ts +87 -0
- package/src/components/css/file-upload/file-upload.css +92 -0
- package/src/components/css/file-upload/file-upload.ts +65 -0
- package/src/components/css/heading/heading.css +33 -0
- package/src/components/css/heading/heading.ts +42 -0
- package/src/components/css/help-text/help-text.css +24 -0
- package/src/components/css/help-text/help-text.ts +42 -0
- package/src/components/css/input/input.css +131 -0
- package/src/components/css/input/input.ts +76 -0
- package/src/components/css/label/label.css +34 -0
- package/src/components/css/label/label.ts +51 -0
- package/src/components/css/link/link.css +36 -0
- package/src/components/css/link/link.ts +29 -0
- package/src/components/css/list/list.css +40 -0
- package/src/components/css/list/list.ts +42 -0
- package/src/components/css/pagination/pagination.css +77 -0
- package/src/components/css/pagination/pagination.ts +33 -0
- package/src/components/css/paragraph/paragraph.css +31 -0
- package/src/components/css/paragraph/paragraph.ts +53 -0
- package/src/components/css/radio/radio.css +76 -0
- package/src/components/css/radio/radio.ts +50 -0
- package/src/components/css/search/search.css +49 -0
- package/src/components/css/search/search.ts +52 -0
- package/src/components/css/select/select.css +95 -0
- package/src/components/css/select/select.ts +40 -0
- package/src/components/css/shared.ts +40 -0
- package/src/components/css/skeleton/skeleton.css +49 -0
- package/src/components/css/skeleton/skeleton.ts +43 -0
- package/src/components/css/skip-link/skip-link.css +36 -0
- package/src/components/css/skip-link/skip-link.ts +22 -0
- package/src/components/css/spinner/spinner.css +48 -0
- package/src/components/css/spinner/spinner.ts +48 -0
- package/src/components/css/sr-only/sr-only.css +16 -0
- package/src/components/css/sr-only/sr-only.ts +18 -0
- package/src/components/css/switch/switch.css +101 -0
- package/src/components/css/switch/switch.ts +46 -0
- package/src/components/css/table/table.css +63 -0
- package/src/components/css/table/table.ts +50 -0
- package/src/components/css/tag/tag.css +58 -0
- package/src/components/css/tag/tag.ts +49 -0
- package/src/components/css/textarea/textarea.css +71 -0
- package/src/components/css/textarea/textarea.ts +40 -0
- package/src/components/css/toggle-group/toggle-group.css +82 -0
- package/src/components/css/toggle-group/toggle-group.ts +37 -0
- package/src/components/css/tooltip/tooltip.css +47 -0
- package/src/components/css/tooltip/tooltip.ts +36 -0
- package/src/components/frittstaende/calendar/fs-calendar.ts +525 -0
- package/src/components/frittstaende/date-field/date-field.css +54 -0
- package/src/components/frittstaende/date-field/fs-date-field.ts +309 -0
- package/src/components/frittstaende/suggestion/fs-suggestion.ts +333 -0
- package/src/components/frittstaende/suggestion/suggestion.css +63 -0
- package/src/components/frittstaende/toast/fs-toast.ts +146 -0
- package/src/components/frittstaende/toast/toast.css +71 -0
- package/src/components/ramme/error-summary/error-summary.css +45 -0
- package/src/components/ramme/error-summary/fs-error-summary.ts +197 -0
- package/src/components/ramme/field/field-core.ts +132 -0
- package/src/components/ramme/field/field.css +6 -0
- package/src/components/ramme/field/fs-field.ts +154 -0
- package/src/components/ramme/popover/fs-popover.ts +205 -0
- package/src/components/ramme/popover/popover.css +34 -0
- package/src/components/ramme/tabs/fs-tabs.ts +182 -0
- package/src/components/ramme/tabs/tabs.css +68 -0
- package/src/tailwind/tailwind.css +134 -0
- package/src/tokens/color.ts +267 -0
- package/src/tokens/theme.ts +350 -0
- package/src/tokens/tokens.css +214 -0
- 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
|
+
}
|