@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/dist/components/css/select/select.d.ts +18 -1
  4. package/dist/components/css/select/select.d.ts.map +1 -1
  5. package/dist/components/css/select/select.js +15 -2
  6. package/dist/components/css/select/select.js.map +1 -1
  7. package/dist/components/frittstaende/session-timeout/fs-session-timeout.d.ts.map +1 -1
  8. package/dist/components/frittstaende/session-timeout/fs-session-timeout.js +5 -1
  9. package/dist/components/frittstaende/session-timeout/fs-session-timeout.js.map +1 -1
  10. package/dist/components/ramme/error-summary/fs-error-summary.d.ts.map +1 -1
  11. package/dist/components/ramme/error-summary/fs-error-summary.js +12 -3
  12. package/dist/components/ramme/error-summary/fs-error-summary.js.map +1 -1
  13. package/dist/components/ramme/field/field-core.d.ts +27 -12
  14. package/dist/components/ramme/field/field-core.d.ts.map +1 -1
  15. package/dist/components/ramme/field/field-core.js +27 -12
  16. package/dist/components/ramme/field/field-core.js.map +1 -1
  17. package/dist/components/ramme/field/fs-field.d.ts +4 -3
  18. package/dist/components/ramme/field/fs-field.d.ts.map +1 -1
  19. package/dist/components/ramme/field/fs-field.js +4 -3
  20. package/dist/components/ramme/field/fs-field.js.map +1 -1
  21. package/dist/components/ramme/suggestion/fs-suggestion.d.ts +11 -1
  22. package/dist/components/ramme/suggestion/fs-suggestion.d.ts.map +1 -1
  23. package/dist/components/ramme/suggestion/fs-suggestion.js +24 -4
  24. package/dist/components/ramme/suggestion/fs-suggestion.js.map +1 -1
  25. package/dist/components/ramme/suggestion/suggestion.d.ts +10 -0
  26. package/dist/components/ramme/suggestion/suggestion.d.ts.map +1 -1
  27. package/dist/components/ramme/suggestion/suggestion.js +13 -1
  28. package/dist/components/ramme/suggestion/suggestion.js.map +1 -1
  29. package/dist/dom.d.ts.map +1 -1
  30. package/dist/dom.js +2 -0
  31. package/dist/dom.js.map +1 -1
  32. package/dist/fs.d.ts +5 -1
  33. package/dist/fs.d.ts.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/react.d.ts +6 -2
  39. package/dist/react.d.ts.map +1 -1
  40. package/dist/react.js +9 -3
  41. package/dist/react.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/components/css/select/select.css +193 -5
  44. package/src/components/css/select/select.ts +24 -1
  45. package/src/components/frittstaende/session-timeout/fs-session-timeout.ts +5 -1
  46. package/src/components/ramme/error-summary/fs-error-summary.ts +13 -3
  47. package/src/components/ramme/field/field-core.ts +27 -12
  48. package/src/components/ramme/field/fs-field.ts +4 -3
  49. package/src/components/ramme/suggestion/fs-suggestion.ts +27 -4
  50. 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
- ({ state = "default" }: SelectOptions = {}): SelectAttributes =>
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
- // Boksen er skjult av serveren når lista er tom. Er den synlig nå, og vi
81
- // ikke har flyttet fokus hit ennå, er det denne innsendingen som feilet.
82
- if (this.shouldFocus && !this.hasFocused && !this.hidden) {
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
- * Kjører serveren JavaScript, skal den kalle `computeFieldAttributes` og
138
- * skrive dem selv. Da trengs verken komponenten eller denne lista. Kjører den
139
- * noe annet, altså Go, PHP eller .NET, gjør komponenten koblingen på klienten,
140
- * og da serveren skrive lista som `data-preserve-attr` hvert av de tre
141
- * elementene. Datastars morfing leser lista fra serverens node, per element,
142
- * komponenten kan ikke beskytte seg selv.
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 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" for="epost"
151
- * data-preserve-attr="class data-required data-optional aria-disabled">E-post</label>
152
- * <input class="fs-input" id="epost"
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-error-text" hidden data-preserve-attr="hidden">Skriv en gyldig adresse.</p>
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 servere som ikke kan kalle `fs.field()`, altså alt som
25
- * ikke kjører JavaScript. Kan serveren kalle byggeren, skal den skrive
26
- * attributtene selv, og da trengs ikke dette elementet.
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
- this.markActive(this.activeIndex - 1)
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
- empty: { class: SUGGESTION_EMPTY_CLASS },
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
  *