@fristil/designsystem 0.5.0 → 0.5.2
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 +101 -0
- package/README.md +1 -1
- package/dist/components/css/select/select.d.ts +18 -1
- package/dist/components/css/select/select.d.ts.map +1 -1
- package/dist/components/css/select/select.js +15 -2
- package/dist/components/css/select/select.js.map +1 -1
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.d.ts.map +1 -1
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.js +5 -1
- package/dist/components/frittstaende/session-timeout/fs-session-timeout.js.map +1 -1
- package/dist/components/ramme/error-summary/fs-error-summary.d.ts.map +1 -1
- package/dist/components/ramme/error-summary/fs-error-summary.js +12 -3
- package/dist/components/ramme/error-summary/fs-error-summary.js.map +1 -1
- package/dist/components/ramme/field/field-core.d.ts +27 -12
- package/dist/components/ramme/field/field-core.d.ts.map +1 -1
- package/dist/components/ramme/field/field-core.js +27 -12
- package/dist/components/ramme/field/field-core.js.map +1 -1
- package/dist/components/ramme/field/fs-field.d.ts +4 -3
- package/dist/components/ramme/field/fs-field.d.ts.map +1 -1
- package/dist/components/ramme/field/fs-field.js +4 -3
- package/dist/components/ramme/field/fs-field.js.map +1 -1
- package/dist/components/ramme/suggestion/fs-suggestion.d.ts +11 -1
- package/dist/components/ramme/suggestion/fs-suggestion.d.ts.map +1 -1
- package/dist/components/ramme/suggestion/fs-suggestion.js +24 -4
- package/dist/components/ramme/suggestion/fs-suggestion.js.map +1 -1
- package/dist/components/ramme/suggestion/suggestion.d.ts +10 -0
- package/dist/components/ramme/suggestion/suggestion.d.ts.map +1 -1
- package/dist/components/ramme/suggestion/suggestion.js +13 -1
- package/dist/components/ramme/suggestion/suggestion.js.map +1 -1
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +2 -0
- package/dist/dom.js.map +1 -1
- package/dist/fs.d.ts +5 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +6 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +9 -3
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/components/css/select/select.css +193 -5
- package/src/components/css/select/select.ts +24 -1
- package/src/components/frittstaende/session-timeout/fs-session-timeout.ts +5 -1
- package/src/components/ramme/error-summary/fs-error-summary.ts +13 -3
- package/src/components/ramme/field/field-core.ts +27 -12
- package/src/components/ramme/field/fs-field.ts +4 -3
- package/src/components/ramme/suggestion/fs-suggestion.ts +27 -4
- package/src/components/ramme/suggestion/suggestion.ts +13 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
attributes,
|
|
3
|
+
createGuard,
|
|
3
4
|
type FieldState,
|
|
4
5
|
fieldStates,
|
|
5
6
|
isFieldState,
|
|
@@ -8,14 +9,29 @@ import {
|
|
|
8
9
|
|
|
9
10
|
export const SELECT_CLASS = "fs-select" as const
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Hvem som tegner nedtrekkslista.
|
|
14
|
+
*
|
|
15
|
+
* `auto` er nettleserens egen liste, et vindu fra operativsystemet som ikke
|
|
16
|
+
* kan styles. `styled` ber nettleseren tegne den inne i siden i stedet, slik
|
|
17
|
+
* at den får designsystemets farger, avstander og skygge.
|
|
18
|
+
*/
|
|
19
|
+
export const selectPickers = ["auto", "styled"] as const
|
|
20
|
+
export type SelectPicker = (typeof selectPickers)[number]
|
|
21
|
+
export type NonDefaultSelectPicker = Exclude<SelectPicker, "auto">
|
|
22
|
+
export const isSelectPicker = createGuard(selectPickers)
|
|
23
|
+
|
|
11
24
|
export type SelectOptions = {
|
|
12
25
|
/** Valideringstilstand. Standard: `default`. */
|
|
13
26
|
state?: FieldState
|
|
27
|
+
/** Hvem som tegner nedtrekkslista. Standard: `auto`. */
|
|
28
|
+
picker?: SelectPicker
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
export type SelectAttributes = {
|
|
17
32
|
class: typeof SELECT_CLASS
|
|
18
33
|
"data-state"?: NonDefaultFieldState
|
|
34
|
+
"data-picker"?: NonDefaultSelectPicker
|
|
19
35
|
"aria-invalid"?: "true"
|
|
20
36
|
}
|
|
21
37
|
|
|
@@ -24,17 +40,24 @@ export type SelectAttributes = {
|
|
|
24
40
|
*
|
|
25
41
|
* ```ts
|
|
26
42
|
* <select {...select({ state: "invalid" })} />
|
|
43
|
+
* <select {...select({ picker: "styled" })} />
|
|
27
44
|
* ```
|
|
28
45
|
*/
|
|
29
46
|
export const select = Object.assign(
|
|
30
|
-
({
|
|
47
|
+
({
|
|
48
|
+
state = "default",
|
|
49
|
+
picker = "auto",
|
|
50
|
+
}: SelectOptions = {}): SelectAttributes =>
|
|
31
51
|
attributes({
|
|
32
52
|
class: SELECT_CLASS,
|
|
33
53
|
"data-state": state === "default" ? undefined : state,
|
|
54
|
+
"data-picker": picker === "auto" ? undefined : picker,
|
|
34
55
|
"aria-invalid": state === "invalid" ? ("true" as const) : undefined,
|
|
35
56
|
}),
|
|
36
57
|
{
|
|
37
58
|
states: fieldStates,
|
|
38
59
|
isState: isFieldState,
|
|
60
|
+
pickers: selectPickers,
|
|
61
|
+
isPicker: isSelectPicker,
|
|
39
62
|
},
|
|
40
63
|
)
|
|
@@ -69,8 +69,11 @@ export class FsSessionTimeout extends HTMLElement {
|
|
|
69
69
|
|
|
70
70
|
connectedCallback(): void {
|
|
71
71
|
this.classList.add(SESSION_TIMEOUT_CLASS)
|
|
72
|
-
this.build()
|
|
73
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.
|
|
74
77
|
for (const navn of ["pointerdown", "keydown", "scroll"] as const) {
|
|
75
78
|
document.addEventListener(navn, this.registerActivity, { passive: true })
|
|
76
79
|
}
|
|
@@ -171,6 +174,7 @@ export class FsSessionTimeout extends HTMLElement {
|
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
private open(): void {
|
|
177
|
+
this.build()
|
|
174
178
|
if (!this.dialog || this.dialog.open) return
|
|
175
179
|
// Fokus skal tilbake dit brukeren var. Uten dette starter neste
|
|
176
180
|
// tastetrykk på toppen av siden, midt i et skjema.
|
|
@@ -77,9 +77,19 @@ export class FsErrorSummary extends HTMLElement {
|
|
|
77
77
|
this.links.add(link)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
80
|
+
// Skjuler serveren boksen igjen, er den innsendingen over. Uten denne
|
|
81
|
+
// nullstillingen tok boksen fokus bare første gang: mønsteret i en
|
|
82
|
+
// Datastar-app er at lista står med de samme lenkene hele veien og bare
|
|
83
|
+
// `hidden` slås av og på, så «har jeg flyttet fokus hit før» er ikke et
|
|
84
|
+
// svar på om dette er en ny innsending.
|
|
85
|
+
if (this.hidden) {
|
|
86
|
+
this.hasFocused = false
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Er boksen synlig nå, og vi ikke har flyttet fokus hit ennå, er det
|
|
91
|
+
// denne innsendingen som feilet.
|
|
92
|
+
if (this.shouldFocus && !this.hasFocused) {
|
|
83
93
|
this.hasFocused = true
|
|
84
94
|
this.focus()
|
|
85
95
|
}
|
|
@@ -134,31 +134,46 @@ export function computeFieldAttributes(
|
|
|
134
134
|
/**
|
|
135
135
|
* Attributtene `<fs-field>` setter, per element den rører.
|
|
136
136
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
137
|
+
* Lista trengs bare når to ting gjelder samtidig.
|
|
138
|
+
*
|
|
139
|
+
* Lages markupen med JavaScript, skal den kalle `computeFieldAttributes` og
|
|
140
|
+
* skrive attributtene selv. Da trengs verken komponenten eller denne lista.
|
|
141
|
+
* Blir markupen til uten JavaScript, i en Go-mal eller håndskrevet HTML, gjør
|
|
142
|
+
* `<fs-field>` koblingen i nettleseren, og på en side serveren sender én gang
|
|
143
|
+
* er det alt som skal til.
|
|
144
|
+
*
|
|
145
|
+
* Oppdaterer serveren det samme området mens brukeren holder på, som i en
|
|
146
|
+
* Datastar- eller htmx-app, må malen i tillegg skrive lista som
|
|
147
|
+
* `data-preserve-attr` på hvert av de tre elementene. Morfingen leser den fra
|
|
148
|
+
* serverens node, per element, så komponenten kan ikke beskytte seg selv.
|
|
143
149
|
*
|
|
144
150
|
* Det holder ikke å beskytte kontrollen alene. Komponenten legger også
|
|
145
151
|
* `fs-label` og markeringene på ledeteksten, og skjuler feilmeldingen. Uten
|
|
146
152
|
* lister på de to andre forsvinner klassen og feilmeldingen ved neste
|
|
147
153
|
* morfing, som er nøyaktig den feilen kontrakten skal hindre.
|
|
148
154
|
*
|
|
155
|
+
* Hjelpeteksten og feilmeldingen må ha `id` med i lista selv om malen ikke
|
|
156
|
+
* skriver noen. Komponenten lager dem i så fall selv, og `aria-describedby`
|
|
157
|
+
* på kontrollen peker på dem. Forsvinner id-en i en morfing, peker
|
|
158
|
+
* koblingen på noe som ikke finnes, og skjermleseren leser verken
|
|
159
|
+
* hjelpeteksten eller feilen. `for` på ledeteksten er der av samme grunn.
|
|
160
|
+
*
|
|
149
161
|
* ```html
|
|
150
|
-
* <label class="fs-label"
|
|
151
|
-
* data-preserve-attr="class data-required data-optional aria-disabled">E-post</label>
|
|
152
|
-
* <input class="fs-input"
|
|
162
|
+
* <label class="fs-label"
|
|
163
|
+
* data-preserve-attr="class for data-required data-optional aria-disabled">E-post</label>
|
|
164
|
+
* <input class="fs-input"
|
|
153
165
|
* data-preserve-attr="id aria-describedby aria-invalid data-state disabled aria-disabled">
|
|
154
|
-
* <p class="fs-
|
|
166
|
+
* <p class="fs-help-text" data-preserve-attr="id">Vi sender aldri spam.</p>
|
|
167
|
+
* <p class="fs-error-text" hidden data-preserve-attr="id hidden">Skriv en gyldig adresse.</p>
|
|
155
168
|
* ```
|
|
156
169
|
*/
|
|
157
170
|
export const FIELD_PRESERVED_ATTRIBUTES = {
|
|
158
171
|
/** På ledeteksten. */
|
|
159
|
-
label: "class data-required data-optional aria-disabled",
|
|
172
|
+
label: "class for data-required data-optional aria-disabled",
|
|
160
173
|
/** På selve kontrollen. */
|
|
161
174
|
control: "id aria-describedby aria-invalid data-state disabled aria-disabled",
|
|
175
|
+
/** På hjelpeteksten. */
|
|
176
|
+
help: "id",
|
|
162
177
|
/** På feilmeldingen. */
|
|
163
|
-
error: "hidden",
|
|
178
|
+
error: "id hidden",
|
|
164
179
|
} as const
|
|
@@ -21,9 +21,10 @@ function setOrRemove(
|
|
|
21
21
|
/**
|
|
22
22
|
* Kobler ledetekst, kontroll, hjelpetekst og feilmelding i vanlig DOM.
|
|
23
23
|
*
|
|
24
|
-
* Komponenten er for
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Komponenten er for markup som blir til uten JavaScript: en Go-mal, en
|
|
25
|
+
* PHP-fil, en Razor-visning eller håndskrevet HTML. Lages markupen med
|
|
26
|
+
* JavaScript, uansett hvor koden kjører, skal `fs.field()` skrive
|
|
27
|
+
* attributtene i stedet, og da trengs ikke dette elementet.
|
|
27
28
|
*
|
|
28
29
|
* Komponenten rendrer ingenting. Den satte tidligere et `<slot>`-element inn i
|
|
29
30
|
* vanlig DOM, og siden serveren ikke visste om det, fjernet Datastars morfing
|
|
@@ -38,7 +38,6 @@ export const FS_SUGGESTION_TAG = "fs-suggestion" as const
|
|
|
38
38
|
export class FsSuggestion extends HTMLElement {
|
|
39
39
|
static observedAttributes = ["server-filtered"]
|
|
40
40
|
|
|
41
|
-
private activeIndex = -1
|
|
42
41
|
private observer?: MutationObserver
|
|
43
42
|
private control?: HTMLInputElement
|
|
44
43
|
/** Alternativene som alt har fått lytteren sin. */
|
|
@@ -75,6 +74,22 @@ export class FsSuggestion extends HTMLElement {
|
|
|
75
74
|
]
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Hvilket synlig alternativ som er markert nå.
|
|
79
|
+
*
|
|
80
|
+
* Den leses fra `aria-selected` i markupen, ikke fra en teller inni
|
|
81
|
+
* komponenten. Serveren kan sette markeringen selv med
|
|
82
|
+
* `fs.suggestion({ activeIndex })`, og en parallell teller ville hoppet
|
|
83
|
+
* til toppen av lista ved første piltast i stedet for til alternativet
|
|
84
|
+
* etter. Filtreringen flytter også på alternativene, og da svarer
|
|
85
|
+
* oppslaget riktig av seg selv.
|
|
86
|
+
*/
|
|
87
|
+
private get activeIndex(): number {
|
|
88
|
+
return this.visible.findIndex(
|
|
89
|
+
(option) => option.getAttribute("aria-selected") === "true",
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
/** Alternativene som er synlige nå. */
|
|
79
94
|
private get visible(): HTMLElement[] {
|
|
80
95
|
return this.options.filter((option) => !option.hidden)
|
|
@@ -120,7 +135,6 @@ export class FsSuggestion extends HTMLElement {
|
|
|
120
135
|
this.control.setAttribute("aria-expanded", String(open))
|
|
121
136
|
|
|
122
137
|
if (!open) {
|
|
123
|
-
this.activeIndex = -1
|
|
124
138
|
this.control.removeAttribute("aria-activedescendant")
|
|
125
139
|
for (const option of this.options) {
|
|
126
140
|
option.setAttribute("aria-selected", "false")
|
|
@@ -166,7 +180,6 @@ export class FsSuggestion extends HTMLElement {
|
|
|
166
180
|
if (synlige.length === 0 || !this.control) return
|
|
167
181
|
|
|
168
182
|
const neste = (index + synlige.length) % synlige.length
|
|
169
|
-
this.activeIndex = neste
|
|
170
183
|
|
|
171
184
|
for (const option of this.options) {
|
|
172
185
|
option.setAttribute("aria-selected", "false")
|
|
@@ -228,7 +241,17 @@ export class FsSuggestion extends HTMLElement {
|
|
|
228
241
|
this.markActive(this.activeIndex + 1)
|
|
229
242
|
} else if (event.key === "ArrowUp") {
|
|
230
243
|
event.preventDefault()
|
|
231
|
-
|
|
244
|
+
// Lista må åpnes først, som med pil ned. Uten det pekte
|
|
245
|
+
// `aria-activedescendant` på et alternativ i en liste feltet samtidig
|
|
246
|
+
// meldte som lukket, og skjermleseren leste opp noe som ikke sto på
|
|
247
|
+
// skjermen.
|
|
248
|
+
if (!åpen) {
|
|
249
|
+
this.filter()
|
|
250
|
+
this.setOpen(true)
|
|
251
|
+
}
|
|
252
|
+
// Er ingenting markert, går pil opp til det siste. `activeIndex - 1`
|
|
253
|
+
// ville gitt det nest siste, siden ingenting markert er -1.
|
|
254
|
+
this.markActive(this.activeIndex < 0 ? -1 : this.activeIndex - 1)
|
|
232
255
|
} else if (event.key === "Enter" && åpen && this.activeIndex >= 0) {
|
|
233
256
|
event.preventDefault()
|
|
234
257
|
const valgt = this.visible[this.activeIndex]
|
|
@@ -82,7 +82,19 @@ export const suggestion = ({
|
|
|
82
82
|
"data-preserve-attr": "aria-selected hidden" as const,
|
|
83
83
|
}),
|
|
84
84
|
),
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Teksten som vises når ingenting passer.
|
|
87
|
+
*
|
|
88
|
+
* Den er skjult så lenge serveren sender noe å velge mellom, og
|
|
89
|
+
* komponenten skjuler og viser den igjen mens brukeren skriver.
|
|
90
|
+
* `data-preserve-attr` må derfor ha `hidden` med: uten den dukket
|
|
91
|
+
* «Ingen treff» opp igjen ved hver patch, også når noe passet.
|
|
92
|
+
*/
|
|
93
|
+
empty: attributes({
|
|
94
|
+
class: SUGGESTION_EMPTY_CLASS,
|
|
95
|
+
hidden: count > 0 ? (true as const) : undefined,
|
|
96
|
+
"data-preserve-attr": "hidden" as const,
|
|
97
|
+
}),
|
|
86
98
|
/**
|
|
87
99
|
* Området som melder antall treff til skjermlesere.
|
|
88
100
|
*
|