@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,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
|
+
}
|