@fristil/designsystem 0.4.0 → 0.5.1
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 +136 -0
- package/README.md +22 -7
- package/dist/components/frittstaende/connection-status/connection-status.d.ts +28 -0
- package/dist/components/frittstaende/connection-status/connection-status.d.ts.map +1 -0
- package/dist/components/frittstaende/connection-status/connection-status.js +22 -0
- package/dist/components/frittstaende/connection-status/connection-status.js.map +1 -0
- package/dist/components/frittstaende/connection-status/fs-connection-status.d.ts +54 -0
- package/dist/components/frittstaende/connection-status/fs-connection-status.d.ts.map +1 -0
- package/dist/components/frittstaende/connection-status/fs-connection-status.js +123 -0
- package/dist/components/frittstaende/connection-status/fs-connection-status.js.map +1 -0
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.d.ts +56 -0
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.d.ts.map +1 -0
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.js +183 -0
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.js.map +1 -0
- package/dist/components/frittstaende/session-timeout/session-timeout.d.ts +28 -0
- package/dist/components/frittstaende/session-timeout/session-timeout.d.ts.map +1 -0
- package/dist/components/frittstaende/session-timeout/session-timeout.js +22 -0
- package/dist/components/frittstaende/session-timeout/session-timeout.js.map +1 -0
- package/dist/components/frittstaende/toast/fs-toast.d.ts +16 -22
- package/dist/components/frittstaende/toast/fs-toast.d.ts.map +1 -1
- package/dist/components/frittstaende/toast/fs-toast.js +38 -27
- package/dist/components/frittstaende/toast/fs-toast.js.map +1 -1
- package/dist/components/frittstaende/toast/toast.d.ts +34 -0
- package/dist/components/frittstaende/toast/toast.d.ts.map +1 -0
- package/dist/components/frittstaende/toast/toast.js +26 -0
- package/dist/components/frittstaende/toast/toast.js.map +1 -0
- package/dist/components/ramme/error-summary/error-summary.d.ts +39 -0
- package/dist/components/ramme/error-summary/error-summary.d.ts.map +1 -0
- package/dist/components/ramme/error-summary/error-summary.js +31 -0
- package/dist/components/ramme/error-summary/error-summary.js.map +1 -0
- package/dist/components/ramme/error-summary/fs-error-summary.d.ts +16 -42
- package/dist/components/ramme/error-summary/fs-error-summary.d.ts.map +1 -1
- package/dist/components/ramme/error-summary/fs-error-summary.js +57 -92
- package/dist/components/ramme/error-summary/fs-error-summary.js.map +1 -1
- package/dist/components/ramme/field/field-core.d.ts +37 -0
- package/dist/components/ramme/field/field-core.d.ts.map +1 -1
- package/dist/components/ramme/field/field-core.js +37 -0
- package/dist/components/ramme/field/field-core.js.map +1 -1
- package/dist/components/ramme/field/fs-field.d.ts +39 -40
- package/dist/components/ramme/field/fs-field.d.ts.map +1 -1
- package/dist/components/ramme/field/fs-field.js +128 -54
- package/dist/components/ramme/field/fs-field.js.map +1 -1
- package/dist/components/ramme/popover/fs-popover.d.ts +20 -27
- package/dist/components/ramme/popover/fs-popover.d.ts.map +1 -1
- package/dist/components/ramme/popover/fs-popover.js +68 -54
- package/dist/components/ramme/popover/fs-popover.js.map +1 -1
- package/dist/components/ramme/popover/popover.d.ts +43 -0
- package/dist/components/ramme/popover/popover.d.ts.map +1 -0
- package/dist/components/ramme/popover/popover.js +49 -0
- package/dist/components/ramme/popover/popover.js.map +1 -0
- package/dist/components/ramme/suggestion/fs-suggestion.d.ts +75 -0
- package/dist/components/ramme/suggestion/fs-suggestion.d.ts.map +1 -0
- package/dist/components/ramme/suggestion/fs-suggestion.js +232 -0
- package/dist/components/ramme/suggestion/fs-suggestion.js.map +1 -0
- package/dist/components/ramme/suggestion/suggestion.d.ts +97 -0
- package/dist/components/ramme/suggestion/suggestion.d.ts.map +1 -0
- package/dist/components/ramme/suggestion/suggestion.js +79 -0
- package/dist/components/ramme/suggestion/suggestion.js.map +1 -0
- package/dist/components/ramme/tabs/fs-tabs.d.ts +31 -36
- package/dist/components/ramme/tabs/fs-tabs.d.ts.map +1 -1
- package/dist/components/ramme/tabs/fs-tabs.js +62 -84
- package/dist/components/ramme/tabs/fs-tabs.js.map +1 -1
- package/dist/components/ramme/tabs/tabs.d.ts +50 -0
- package/dist/components/ramme/tabs/tabs.d.ts.map +1 -0
- package/dist/components/ramme/tabs/tabs.js +54 -0
- package/dist/components/ramme/tabs/tabs.js.map +1 -0
- package/dist/fs.d.ts +94 -0
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +42 -0
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/jsx/react.d.ts +35 -50
- package/dist/jsx/react.d.ts.map +1 -1
- package/dist/react.d.ts +142 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +77 -2
- package/dist/react.js.map +1 -1
- package/package.json +15 -18
- package/src/components/frittstaende/connection-status/connection-status.css +33 -0
- package/src/components/frittstaende/connection-status/connection-status.ts +41 -0
- package/src/components/frittstaende/connection-status/fs-connection-status.ts +144 -0
- package/src/components/frittstaende/session-timeout/fs-session-timeout.ts +216 -0
- package/src/components/frittstaende/session-timeout/session-timeout.css +43 -0
- package/src/components/frittstaende/session-timeout/session-timeout.ts +42 -0
- package/src/components/frittstaende/toast/fs-toast.ts +39 -30
- package/src/components/frittstaende/toast/toast.ts +45 -0
- package/src/components/ramme/error-summary/error-summary.ts +55 -0
- package/src/components/ramme/error-summary/fs-error-summary.ts +62 -119
- package/src/components/ramme/field/field-core.ts +38 -0
- package/src/components/ramme/field/fs-field.ts +145 -61
- package/src/components/ramme/popover/fs-popover.ts +78 -72
- package/src/components/ramme/popover/popover.ts +79 -0
- package/src/components/ramme/suggestion/fs-suggestion.ts +265 -0
- package/src/components/ramme/suggestion/suggestion.ts +105 -0
- package/src/components/ramme/tabs/fs-tabs.ts +67 -98
- package/src/components/ramme/tabs/tabs.ts +104 -0
- package/dist/components/frittstaende/calendar/fs-calendar.d.ts +0 -52
- package/dist/components/frittstaende/calendar/fs-calendar.d.ts.map +0 -1
- package/dist/components/frittstaende/calendar/fs-calendar.js +0 -462
- package/dist/components/frittstaende/calendar/fs-calendar.js.map +0 -1
- package/dist/components/frittstaende/date-field/fs-date-field.d.ts +0 -110
- package/dist/components/frittstaende/date-field/fs-date-field.d.ts.map +0 -1
- package/dist/components/frittstaende/date-field/fs-date-field.js +0 -264
- package/dist/components/frittstaende/date-field/fs-date-field.js.map +0 -1
- package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts +0 -133
- package/dist/components/frittstaende/suggestion/fs-suggestion.d.ts.map +0 -1
- package/dist/components/frittstaende/suggestion/fs-suggestion.js +0 -280
- package/dist/components/frittstaende/suggestion/fs-suggestion.js.map +0 -1
- package/src/components/frittstaende/calendar/fs-calendar.ts +0 -525
- package/src/components/frittstaende/date-field/date-field.css +0 -54
- package/src/components/frittstaende/date-field/fs-date-field.ts +0 -309
- package/src/components/frittstaende/suggestion/fs-suggestion.ts +0 -333
- /package/src/components/{frittstaende → ramme}/suggestion/suggestion.css +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONNECTION_STATUS_BAR_CLASS,
|
|
3
|
+
CONNECTION_STATUS_CLASS,
|
|
4
|
+
} from "./connection-status.js"
|
|
5
|
+
|
|
6
|
+
export const FS_CONNECTION_STATUS_TAG = "fs-connection-status" as const
|
|
7
|
+
|
|
8
|
+
const STANDARD_OFFLINE = "Ingen forbindelse. Det du skriver blir ikke lagret."
|
|
9
|
+
const STANDARD_ONLINE = "Forbindelsen er tilbake."
|
|
10
|
+
|
|
11
|
+
/** Hvor lenge «tilbake på nett» blir stående. */
|
|
12
|
+
const KVITTERING_MS = 4000
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sier fra når forbindelsen til serveren er borte.
|
|
16
|
+
*
|
|
17
|
+
* Serveren kan per definisjon ikke fortelle deg dette. Er den utilgjengelig,
|
|
18
|
+
* kommer det ingenting derfra. Derfor er dette en frittstående komponent som
|
|
19
|
+
* eier alt innholdet sitt.
|
|
20
|
+
*
|
|
21
|
+
* Den er ikke pynt i en app som henter innhold fra serveren underveis. En
|
|
22
|
+
* side som mister strømmen sin ser helt normal ut, men er frosset, og en
|
|
23
|
+
* bruker som fyller ut et skjema inn i en død forbindelse mister alt.
|
|
24
|
+
*
|
|
25
|
+
* `navigator.onLine` sier bare om maskinen har et nettverk, ikke om serveren
|
|
26
|
+
* svarer. Derfor finnes `reportFailure()` og `reportSuccess()`, som appen
|
|
27
|
+
* kaller når et kall feiler eller lykkes.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const status = document.querySelector("fs-connection-status")
|
|
31
|
+
* try {
|
|
32
|
+
* await fetch("/api/lagre", { method: "POST", body })
|
|
33
|
+
* status.reportSuccess()
|
|
34
|
+
* } catch {
|
|
35
|
+
* status.reportFailure()
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class FsConnectionStatus extends HTMLElement {
|
|
40
|
+
static observedAttributes = ["offline-text", "online-text"]
|
|
41
|
+
|
|
42
|
+
private bar?: HTMLElement
|
|
43
|
+
private timer?: number
|
|
44
|
+
/** Sant når appen har meldt at et kall feilet, uavhengig av navigator. */
|
|
45
|
+
private failing = false
|
|
46
|
+
|
|
47
|
+
get offlineText(): string {
|
|
48
|
+
return this.getAttribute("offline-text") ?? STANDARD_OFFLINE
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get onlineText(): string {
|
|
52
|
+
return this.getAttribute("online-text") ?? STANDARD_ONLINE
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
connectedCallback(): void {
|
|
56
|
+
this.classList.add(CONNECTION_STATUS_CLASS)
|
|
57
|
+
window.addEventListener("offline", this.handleOffline)
|
|
58
|
+
window.addEventListener("online", this.handleOnline)
|
|
59
|
+
if (!navigator.onLine) this.showOffline()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
disconnectedCallback(): void {
|
|
63
|
+
window.removeEventListener("offline", this.handleOffline)
|
|
64
|
+
window.removeEventListener("online", this.handleOnline)
|
|
65
|
+
if (this.timer) window.clearTimeout(this.timer)
|
|
66
|
+
this.timer = undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private ensureBar(): HTMLElement {
|
|
70
|
+
if (this.bar) return this.bar
|
|
71
|
+
|
|
72
|
+
const bar = document.createElement("div")
|
|
73
|
+
bar.className = CONNECTION_STATUS_BAR_CLASS
|
|
74
|
+
// polite og ikke assertive: beskjeden er viktig, men skal ikke avbryte
|
|
75
|
+
// midt i et ord. Den blir stående til forbindelsen er tilbake.
|
|
76
|
+
bar.setAttribute("role", "status")
|
|
77
|
+
bar.setAttribute("aria-live", "polite")
|
|
78
|
+
this.append(bar)
|
|
79
|
+
this.bar = bar
|
|
80
|
+
return bar
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private showOffline(): void {
|
|
84
|
+
if (this.timer) window.clearTimeout(this.timer)
|
|
85
|
+
const bar = this.ensureBar()
|
|
86
|
+
bar.dataset.state = "offline"
|
|
87
|
+
bar.textContent = this.offlineText
|
|
88
|
+
this.emit("connection-lost")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private showOnline(): void {
|
|
92
|
+
if (!this.bar) return
|
|
93
|
+
const bar = this.bar
|
|
94
|
+
bar.dataset.state = "online"
|
|
95
|
+
bar.textContent = this.onlineText
|
|
96
|
+
this.emit("connection-restored")
|
|
97
|
+
|
|
98
|
+
this.timer = window.setTimeout(() => {
|
|
99
|
+
bar.remove()
|
|
100
|
+
this.bar = undefined
|
|
101
|
+
}, KVITTERING_MS)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private handleOffline = (): void => {
|
|
105
|
+
this.showOffline()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private handleOnline = (): void => {
|
|
109
|
+
if (this.failing) return
|
|
110
|
+
this.showOnline()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private emit(navn: string): void {
|
|
114
|
+
this.dispatchEvent(new CustomEvent(navn, { bubbles: true, composed: true }))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Meld fra at et kall til serveren feilet. */
|
|
118
|
+
reportFailure(): void {
|
|
119
|
+
if (this.failing) return
|
|
120
|
+
this.failing = true
|
|
121
|
+
this.showOffline()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Meld fra at et kall til serveren lyktes igjen. */
|
|
125
|
+
reportSuccess(): void {
|
|
126
|
+
if (!this.failing) return
|
|
127
|
+
this.failing = false
|
|
128
|
+
this.showOnline()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
declare global {
|
|
133
|
+
interface HTMLElementTagNameMap {
|
|
134
|
+
"fs-connection-status": FsConnectionStatus
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function defineFsConnectionStatus(
|
|
139
|
+
tagName = FS_CONNECTION_STATUS_TAG,
|
|
140
|
+
): void {
|
|
141
|
+
if (!customElements.get(tagName)) {
|
|
142
|
+
customElements.define(tagName, FsConnectionStatus)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_TIMEOUT_CLASS,
|
|
3
|
+
SESSION_TIMEOUT_DIALOG_CLASS,
|
|
4
|
+
} from "./session-timeout.js"
|
|
5
|
+
|
|
6
|
+
export const FS_SESSION_TIMEOUT_TAG = "fs-session-timeout" as const
|
|
7
|
+
|
|
8
|
+
/** Sekundene som skal leses opp. Hvert sekund ville vært uutholdelig. */
|
|
9
|
+
const ANNONSER_VED = new Set([120, 60, 30, 10])
|
|
10
|
+
|
|
11
|
+
function klokke(sekunder: number): string {
|
|
12
|
+
const m = Math.floor(sekunder / 60)
|
|
13
|
+
const s = sekunder % 60
|
|
14
|
+
return `${m}:${String(s).padStart(2, "0")}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ord(sekunder: number): string {
|
|
18
|
+
if (sekunder >= 60) {
|
|
19
|
+
const m = Math.round(sekunder / 60)
|
|
20
|
+
return m === 1 ? "ett minutt" : `${m} minutter`
|
|
21
|
+
}
|
|
22
|
+
return sekunder === 1 ? "ett sekund" : `${sekunder} sekunder`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Varsler før en innlogget økt går ut.
|
|
27
|
+
*
|
|
28
|
+
* Dette er tingen hver store organisasjon bygger selv og gjør feil. Enten
|
|
29
|
+
* kommer det ingen advarsel, og brukeren mister et halvutfylt søknadsskjema,
|
|
30
|
+
* eller så stjeler dialogen fokus midt i en setning, eller så leser
|
|
31
|
+
* skjermleseren nedtellingen hvert sekund.
|
|
32
|
+
*
|
|
33
|
+
* Komponenten er frittstående fordi nedtellingen er klientens klokke. Tallet
|
|
34
|
+
* endrer seg ut fra når brukeren sist rørte tastaturet, og det er ingenting
|
|
35
|
+
* en server kan sende. Den bærer heller ingen verdi inn i en innsending.
|
|
36
|
+
*
|
|
37
|
+
* Dialogen er en ekte `<dialog>` med `showModal()`, så fokusfellen, Escape og
|
|
38
|
+
* topplaget kommer fra nettleseren. Fokus går tilbake dit brukeren var.
|
|
39
|
+
*
|
|
40
|
+
* ```html
|
|
41
|
+
* <fs-session-timeout class="fs-session-timeout" warn-at="1500" expires-at="1800"
|
|
42
|
+
* data-ignore-morph></fs-session-timeout>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* document.querySelector("fs-session-timeout")
|
|
47
|
+
* .addEventListener("session-extend", () => fetch("/forleng", { method: "POST" }))
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export class FsSessionTimeout extends HTMLElement {
|
|
51
|
+
static observedAttributes = ["warn-at", "expires-at"]
|
|
52
|
+
|
|
53
|
+
private dialog?: HTMLDialogElement
|
|
54
|
+
private countElement?: HTMLElement
|
|
55
|
+
private liveElement?: HTMLElement
|
|
56
|
+
private ticker?: number
|
|
57
|
+
private lastActivity = Date.now()
|
|
58
|
+
private forrigeFokus: HTMLElement | null = null
|
|
59
|
+
|
|
60
|
+
/** Sekunder uten aktivitet før varselet kommer. */
|
|
61
|
+
get warnAt(): number {
|
|
62
|
+
return Number(this.getAttribute("warn-at")) || 25 * 60
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Sekunder uten aktivitet før økten er ute. */
|
|
66
|
+
get expiresAt(): number {
|
|
67
|
+
return Number(this.getAttribute("expires-at")) || 30 * 60
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
connectedCallback(): void {
|
|
71
|
+
this.classList.add(SESSION_TIMEOUT_CLASS)
|
|
72
|
+
|
|
73
|
+
// Dialogen bygges først når den skal vises, ikke her. Lagde vi den med
|
|
74
|
+
// én gang, sto den i DOM-en før React rakk å hydrere, og React fant et
|
|
75
|
+
// element den ikke hadde rendret. `<fs-toast>` og
|
|
76
|
+
// `<fs-connection-status>` gjør det på samme måte.
|
|
77
|
+
for (const navn of ["pointerdown", "keydown", "scroll"] as const) {
|
|
78
|
+
document.addEventListener(navn, this.registerActivity, { passive: true })
|
|
79
|
+
}
|
|
80
|
+
this.ticker = window.setInterval(() => this.tick(), 1000)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disconnectedCallback(): void {
|
|
84
|
+
for (const navn of ["pointerdown", "keydown", "scroll"] as const) {
|
|
85
|
+
document.removeEventListener(navn, this.registerActivity)
|
|
86
|
+
}
|
|
87
|
+
if (this.ticker) window.clearInterval(this.ticker)
|
|
88
|
+
this.ticker = undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private build(): void {
|
|
92
|
+
if (this.dialog) return
|
|
93
|
+
|
|
94
|
+
const dialog = document.createElement("dialog")
|
|
95
|
+
dialog.className = SESSION_TIMEOUT_DIALOG_CLASS
|
|
96
|
+
// alertdialog og ikke dialog: dette er en avbrytelse brukeren ikke ba om,
|
|
97
|
+
// og da skal skjermleseren lese hele innholdet når den åpnes.
|
|
98
|
+
dialog.setAttribute("aria-labelledby", "fs-session-timeout-title")
|
|
99
|
+
dialog.setAttribute("role", "alertdialog")
|
|
100
|
+
|
|
101
|
+
const tittel = document.createElement("h2")
|
|
102
|
+
tittel.className = "fs-session-timeout__title"
|
|
103
|
+
tittel.id = "fs-session-timeout-title"
|
|
104
|
+
tittel.textContent = "Du blir snart logget ut"
|
|
105
|
+
|
|
106
|
+
const tekst = document.createElement("p")
|
|
107
|
+
tekst.className = "fs-session-timeout__text"
|
|
108
|
+
tekst.append("Vi logger deg ut om ")
|
|
109
|
+
|
|
110
|
+
const count = document.createElement("span")
|
|
111
|
+
count.className = "fs-session-timeout__count"
|
|
112
|
+
// Tallet oppdateres hvert sekund. Leses det opp hver gang, blir dialogen
|
|
113
|
+
// ubrukelig med skjermleser, så opplesningen skjer i live-området under.
|
|
114
|
+
count.setAttribute("aria-hidden", "true")
|
|
115
|
+
tekst.append(count, " for å beskytte opplysningene dine.")
|
|
116
|
+
|
|
117
|
+
const live = document.createElement("span")
|
|
118
|
+
live.className = "fs-sr-only"
|
|
119
|
+
live.setAttribute("role", "status")
|
|
120
|
+
live.setAttribute("aria-live", "polite")
|
|
121
|
+
|
|
122
|
+
const knapper = document.createElement("div")
|
|
123
|
+
knapper.className = "fs-session-timeout__actions"
|
|
124
|
+
|
|
125
|
+
const fortsett = document.createElement("button")
|
|
126
|
+
fortsett.type = "button"
|
|
127
|
+
fortsett.className = "fs-button"
|
|
128
|
+
fortsett.textContent = "Fortsett å være innlogget"
|
|
129
|
+
fortsett.addEventListener("click", () => this.extend())
|
|
130
|
+
|
|
131
|
+
const loggUt = document.createElement("button")
|
|
132
|
+
loggUt.type = "button"
|
|
133
|
+
loggUt.className = "fs-button"
|
|
134
|
+
loggUt.dataset.variant = "secondary"
|
|
135
|
+
loggUt.textContent = "Logg ut nå"
|
|
136
|
+
loggUt.addEventListener("click", () => {
|
|
137
|
+
this.close()
|
|
138
|
+
this.emit("session-logout")
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
knapper.append(fortsett, loggUt)
|
|
142
|
+
dialog.append(tittel, tekst, live, knapper)
|
|
143
|
+
this.append(dialog)
|
|
144
|
+
|
|
145
|
+
this.dialog = dialog
|
|
146
|
+
this.countElement = count
|
|
147
|
+
this.liveElement = live
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private registerActivity = (): void => {
|
|
151
|
+
if (this.dialog?.open) return
|
|
152
|
+
this.lastActivity = Date.now()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private tick(): void {
|
|
156
|
+
const gått = Math.floor((Date.now() - this.lastActivity) / 1000)
|
|
157
|
+
const igjen = this.expiresAt - gått
|
|
158
|
+
|
|
159
|
+
if (igjen <= 0) {
|
|
160
|
+
this.close()
|
|
161
|
+
this.emit("session-expired")
|
|
162
|
+
this.lastActivity = Date.now()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (gått < this.warnAt) return
|
|
167
|
+
|
|
168
|
+
if (!this.dialog?.open) this.open()
|
|
169
|
+
|
|
170
|
+
if (this.countElement) this.countElement.textContent = klokke(igjen)
|
|
171
|
+
if (this.liveElement && ANNONSER_VED.has(igjen)) {
|
|
172
|
+
this.liveElement.textContent = `Du blir logget ut om ${ord(igjen)}.`
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private open(): void {
|
|
177
|
+
this.build()
|
|
178
|
+
if (!this.dialog || this.dialog.open) return
|
|
179
|
+
// Fokus skal tilbake dit brukeren var. Uten dette starter neste
|
|
180
|
+
// tastetrykk på toppen av siden, midt i et skjema.
|
|
181
|
+
this.forrigeFokus = document.activeElement as HTMLElement | null
|
|
182
|
+
this.dialog.showModal()
|
|
183
|
+
this.emit("session-warn")
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private close(): void {
|
|
187
|
+
if (!this.dialog?.open) return
|
|
188
|
+
this.dialog.close()
|
|
189
|
+
if (this.liveElement) this.liveElement.textContent = ""
|
|
190
|
+
this.forrigeFokus?.focus()
|
|
191
|
+
this.forrigeFokus = null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private emit(navn: string): void {
|
|
195
|
+
this.dispatchEvent(new CustomEvent(navn, { bubbles: true, composed: true }))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Forlenger økten og lukker varselet. Kall den når serveren har svart. */
|
|
199
|
+
extend(): void {
|
|
200
|
+
this.lastActivity = Date.now()
|
|
201
|
+
this.close()
|
|
202
|
+
this.emit("session-extend")
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
declare global {
|
|
207
|
+
interface HTMLElementTagNameMap {
|
|
208
|
+
"fs-session-timeout": FsSessionTimeout
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function defineFsSessionTimeout(tagName = FS_SESSION_TIMEOUT_TAG): void {
|
|
213
|
+
if (!customElements.get(tagName)) {
|
|
214
|
+
customElements.define(tagName, FsSessionTimeout)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
@import "../../css/button/button.css";
|
|
2
|
+
|
|
3
|
+
@layer fristil {
|
|
4
|
+
fs-session-timeout {
|
|
5
|
+
display: contents;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.fs-session-timeout__dialog {
|
|
9
|
+
max-inline-size: var(--fs-session-timeout-width, 26rem);
|
|
10
|
+
padding: var(--fs-session-timeout-padding, var(--size-5));
|
|
11
|
+
color: var(--semantic-page-foreground);
|
|
12
|
+
background: var(--semantic-page-background);
|
|
13
|
+
border: 1px solid var(--semantic-divider-30);
|
|
14
|
+
border-radius: var(--fs-session-timeout-radius, var(--size-1));
|
|
15
|
+
box-shadow: var(--semantic-shadow-overlay);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.fs-session-timeout__dialog::backdrop {
|
|
19
|
+
background: var(--semantic-overlay-backdrop);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.fs-session-timeout__title {
|
|
23
|
+
margin: 0 0 var(--size-2);
|
|
24
|
+
font-size: var(--font-size-l);
|
|
25
|
+
line-height: 1.3;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.fs-session-timeout__text {
|
|
29
|
+
margin: 0 0 var(--size-4);
|
|
30
|
+
line-height: 1.5;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.fs-session-timeout__count {
|
|
34
|
+
font-variant-numeric: tabular-nums;
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.fs-session-timeout__actions {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
gap: var(--size-2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { attributes } from "../../css/shared.js"
|
|
2
|
+
|
|
3
|
+
export const SESSION_TIMEOUT_CLASS = "fs-session-timeout" as const
|
|
4
|
+
export const SESSION_TIMEOUT_DIALOG_CLASS =
|
|
5
|
+
"fs-session-timeout__dialog" as const
|
|
6
|
+
|
|
7
|
+
export type SessionTimeoutOptions = {
|
|
8
|
+
/** Sekunder uten aktivitet før varselet kommer. Standard: 25 minutter. */
|
|
9
|
+
warnAt?: number
|
|
10
|
+
/** Sekunder uten aktivitet før økten er ute. Standard: 30 minutter. */
|
|
11
|
+
expiresAt?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SessionTimeoutAttributes = {
|
|
15
|
+
class: typeof SESSION_TIMEOUT_CLASS
|
|
16
|
+
"warn-at"?: string
|
|
17
|
+
"expires-at"?: string
|
|
18
|
+
"data-ignore-morph": ""
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Attributtene for varselet om at innlogget økt går ut.
|
|
23
|
+
*
|
|
24
|
+
* Nedtellingen er klientens klokke, og endrer seg hvert sekund ut fra når
|
|
25
|
+
* brukeren sist rørte tastaturet. Det er en tilstand ingen server kan sende,
|
|
26
|
+
* og derfor eier komponenten alt inni seg. `data-ignore-morph` er ikke
|
|
27
|
+
* valgfri: uten den river Datastars morfing dialogen bort mens den står åpen.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* <fs-session-timeout {...fs.sessionTimeout({ warnAt: 1500, expiresAt: 1800 })} />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const sessionTimeout = ({
|
|
34
|
+
warnAt = 25 * 60,
|
|
35
|
+
expiresAt = 30 * 60,
|
|
36
|
+
}: SessionTimeoutOptions = {}): SessionTimeoutAttributes =>
|
|
37
|
+
attributes({
|
|
38
|
+
class: SESSION_TIMEOUT_CLASS,
|
|
39
|
+
"warn-at": warnAt === 25 * 60 ? undefined : String(warnAt),
|
|
40
|
+
"expires-at": expiresAt === 30 * 60 ? undefined : String(expiresAt),
|
|
41
|
+
"data-ignore-morph": "" as const,
|
|
42
|
+
})
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TOAST_CLASS, TOAST_CLOSE_CLASS } from "./toast.js"
|
|
2
2
|
|
|
3
3
|
export const FS_TOAST_TAG = "fs-toast" as const
|
|
4
4
|
|
|
5
|
-
export const TOAST_CLASS = "fs-toast" as const
|
|
6
|
-
export const TOAST_CLOSE_CLASS = "fs-toast__close" as const
|
|
7
|
-
|
|
8
5
|
export const toastColors = ["neutral", "success", "warning", "danger"] as const
|
|
9
6
|
export type ToastColor = (typeof toastColors)[number]
|
|
10
7
|
|
|
11
|
-
export type
|
|
8
|
+
export type ShowOptions = {
|
|
12
9
|
/** Hva meldingen betyr. Standard: `neutral`. */
|
|
13
10
|
color?: ToastColor
|
|
14
11
|
/** Millisekunder før meldingen forsvinner. `0` lar den bli stående. */
|
|
@@ -20,8 +17,15 @@ export type ToastOptions = {
|
|
|
20
17
|
/**
|
|
21
18
|
* Køen av korte meldinger i hjørnet av skjermen.
|
|
22
19
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* Dette er den eneste frittstående komponenten, og den eneste delen av pakken
|
|
21
|
+
* du kaller i stedet for å skrive. Elementet er beholderen, ikke meldingen:
|
|
22
|
+
* du legger det inn én gang i appen og kaller `show()` når noe skal meldes.
|
|
23
|
+
* Meldingene finnes ikke før en hendelse på klienten skaper dem, så det er
|
|
24
|
+
* ingenting for serveren å rendre.
|
|
25
|
+
*
|
|
26
|
+
* Serveren skriver beholderen med `fs.toast()`, og den setter
|
|
27
|
+
* `data-ignore-morph`. Uten det river Datastars morfing meldingene bort ved
|
|
28
|
+
* neste patch, fordi serverens utgave av regionen er tom.
|
|
25
29
|
*
|
|
26
30
|
* Beholderen er en `role="status"`-region, ikke `role="alert"`. En melding
|
|
27
31
|
* som dukker opp i hjørnet skal ikke avbryte det skjermleseren holder på med;
|
|
@@ -39,40 +43,45 @@ export type ToastOptions = {
|
|
|
39
43
|
* })
|
|
40
44
|
* ```
|
|
41
45
|
*/
|
|
42
|
-
export class FsToast extends
|
|
43
|
-
static
|
|
44
|
-
duration: { type: Number },
|
|
45
|
-
label: { type: String },
|
|
46
|
-
}
|
|
46
|
+
export class FsToast extends HTMLElement {
|
|
47
|
+
static observedAttributes = ["duration", "label"]
|
|
47
48
|
|
|
48
49
|
/** Standard levetid i millisekunder. `0` lar meldingene bli stående. */
|
|
49
|
-
duration
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
50
|
+
get duration(): number {
|
|
51
|
+
// `0` er en gyldig verdi og betyr at meldingene blir stående. Uten
|
|
52
|
+
// sjekken mot null her forsvant de likevel etter seks sekunder, og bare
|
|
53
|
+
// `show(..., { duration: 0 })` virket.
|
|
54
|
+
const rå = this.getAttribute("duration")
|
|
55
|
+
if (rå === null) return 6000
|
|
56
|
+
const value = Number(rå)
|
|
57
|
+
return Number.isFinite(value) && value >= 0 ? value : 6000
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
set duration(value: number) {
|
|
61
|
+
this.setAttribute("duration", String(value))
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
connectedCallback() {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.setAttribute("
|
|
64
|
+
connectedCallback(): void {
|
|
65
|
+
// Skrev serveren regionen med fs.toast(), står alt dette allerede. Her
|
|
66
|
+
// settes det bare når det mangler, så en ren HTML-side uten bygger også
|
|
67
|
+
// får en region skjermleseren forstår.
|
|
68
|
+
if (!this.hasAttribute("role")) this.setAttribute("role", "status")
|
|
69
|
+
if (!this.hasAttribute("aria-live")) {
|
|
70
|
+
this.setAttribute("aria-live", "polite")
|
|
71
|
+
}
|
|
72
|
+
if (!this.hasAttribute("aria-label")) {
|
|
73
|
+
this.setAttribute("aria-label", this.getAttribute("label") ?? "Meldinger")
|
|
74
|
+
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
attributeChangedCallback(navn: string, _gammel: string, ny: string): void {
|
|
69
78
|
// `label` kan settes etter at elementet står i DOM-en, for eksempel av et
|
|
70
79
|
// rammeverk som fyller inn attributtene i et senere steg.
|
|
71
|
-
this.setAttribute("aria-label",
|
|
80
|
+
if (navn === "label" && ny) this.setAttribute("aria-label", ny)
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/** Viser en melding, og returnerer elementet den ble lagt i. */
|
|
75
|
-
show(message: string, options:
|
|
84
|
+
show(message: string, options: ShowOptions = {}): HTMLElement {
|
|
76
85
|
const {
|
|
77
86
|
color = "neutral",
|
|
78
87
|
duration = this.duration,
|
|
@@ -116,7 +125,7 @@ export class FsToast extends LitElement {
|
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
/** Fjerner en melding. */
|
|
119
|
-
dismiss(toast: HTMLElement) {
|
|
128
|
+
dismiss(toast: HTMLElement): void {
|
|
120
129
|
if (!this.contains(toast)) return
|
|
121
130
|
toast.remove()
|
|
122
131
|
|
|
@@ -126,7 +135,7 @@ export class FsToast extends LitElement {
|
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
/** Fjerner alle meldingene. */
|
|
129
|
-
clear() {
|
|
138
|
+
clear(): void {
|
|
130
139
|
for (const toast of this.querySelectorAll(`.${TOAST_CLASS}`)) {
|
|
131
140
|
toast.remove()
|
|
132
141
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { attributes } from "../../css/shared.js"
|
|
2
|
+
|
|
3
|
+
export const TOAST_CLASS = "fs-toast" as const
|
|
4
|
+
export const TOAST_CLOSE_CLASS = "fs-toast__close" as const
|
|
5
|
+
|
|
6
|
+
export type ToastOptions = {
|
|
7
|
+
/** Tekst som sier hva regionen er. Blir `aria-label`. */
|
|
8
|
+
label?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ToastAttributes = {
|
|
12
|
+
region: {
|
|
13
|
+
role: "status"
|
|
14
|
+
"aria-live": "polite"
|
|
15
|
+
"aria-label": string
|
|
16
|
+
"data-ignore-morph": ""
|
|
17
|
+
}
|
|
18
|
+
toast: { class: typeof TOAST_CLASS }
|
|
19
|
+
close: { class: typeof TOAST_CLOSE_CLASS }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Attributtene for varselregionen.
|
|
24
|
+
*
|
|
25
|
+
* Regionen er `role="status"` og ikke `role="alert"`. En melding som kommer
|
|
26
|
+
* fordi noe gikk bra skal ikke avbryte det skjermleseren holder på med.
|
|
27
|
+
*
|
|
28
|
+
* `data-ignore-morph` er ikke valgfri. `<fs-toast>` lager og fjerner sine egne
|
|
29
|
+
* elementer inni regionen, og uten attributtet river Datastars morfing dem bort
|
|
30
|
+
* ved neste patch, fordi serverens utgave av regionen er tom. Attributtet må
|
|
31
|
+
* stå i HTML-en serveren sender: komponenten kan ikke sette det på seg selv,
|
|
32
|
+
* siden morfingen leser det fra serverens node.
|
|
33
|
+
*/
|
|
34
|
+
export const toast = ({
|
|
35
|
+
label = "Varsler",
|
|
36
|
+
}: ToastOptions = {}): ToastAttributes => ({
|
|
37
|
+
region: attributes({
|
|
38
|
+
role: "status" as const,
|
|
39
|
+
"aria-live": "polite" as const,
|
|
40
|
+
"aria-label": label,
|
|
41
|
+
"data-ignore-morph": "" as const,
|
|
42
|
+
}),
|
|
43
|
+
toast: { class: TOAST_CLASS },
|
|
44
|
+
close: { class: TOAST_CLOSE_CLASS },
|
|
45
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { attributes } from "../../css/shared.js"
|
|
2
|
+
|
|
3
|
+
export const ERROR_SUMMARY_CLASS = "fs-error-summary" as const
|
|
4
|
+
export const ERROR_SUMMARY_TITLE_CLASS = "fs-error-summary__title" as const
|
|
5
|
+
|
|
6
|
+
export type ErrorSummaryOptions = {
|
|
7
|
+
/** Antall feil serveren fant. Er det null, skjules boksen. */
|
|
8
|
+
count?: number
|
|
9
|
+
/** Id på boksen, slik at skjemaet kan flytte fokus hit etter innsending. */
|
|
10
|
+
id?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ErrorSummaryAttributes = {
|
|
14
|
+
container: {
|
|
15
|
+
class: typeof ERROR_SUMMARY_CLASS
|
|
16
|
+
role: "alert"
|
|
17
|
+
tabindex: "-1"
|
|
18
|
+
id?: string
|
|
19
|
+
hidden?: true
|
|
20
|
+
}
|
|
21
|
+
title: {
|
|
22
|
+
class: typeof ERROR_SUMMARY_TITLE_CLASS
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attributtene for en feiloppsummering.
|
|
28
|
+
*
|
|
29
|
+
* Serveren skriver hele boksen, også overskriften og lista. `<fs-error-summary>`
|
|
30
|
+
* flytter bare fokus og håndterer klikk på lenkene. Tidligere lagde komponenten
|
|
31
|
+
* overskriften selv, og da forsvant den ved hver morfing i Datastar.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const feil = fs.errorSummary({ count: errors.length, id: "feil" })
|
|
35
|
+
* ```
|
|
36
|
+
* ```html
|
|
37
|
+
* <fs-error-summary {...feil.container}>
|
|
38
|
+
* <h2 {...feil.title}>Du må rette 2 feil</h2>
|
|
39
|
+
* <ul><li><a href="#epost">E-posten mangler @</a></li></ul>
|
|
40
|
+
* </fs-error-summary>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const errorSummary = ({
|
|
44
|
+
count = 0,
|
|
45
|
+
id,
|
|
46
|
+
}: ErrorSummaryOptions = {}): ErrorSummaryAttributes => ({
|
|
47
|
+
container: attributes({
|
|
48
|
+
class: ERROR_SUMMARY_CLASS,
|
|
49
|
+
role: "alert" as const,
|
|
50
|
+
tabindex: "-1" as const,
|
|
51
|
+
id,
|
|
52
|
+
hidden: count === 0 ? (true as const) : undefined,
|
|
53
|
+
}),
|
|
54
|
+
title: { class: ERROR_SUMMARY_TITLE_CLASS },
|
|
55
|
+
})
|