@mattilsynet/design 3.2.9 → 3.3.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.
Files changed (90) hide show
  1. package/mtds/ai/AGENTS.md +892 -0
  2. package/mtds/ai/alert.mdx +63 -0
  3. package/mtds/ai/alert.stories.tsx +128 -0
  4. package/mtds/ai/analytics.mdx +185 -0
  5. package/mtds/ai/app.mdx +60 -0
  6. package/mtds/ai/app.stories.tsx +897 -0
  7. package/mtds/ai/atlas.mdx +82 -0
  8. package/mtds/ai/atlas.stories.tsx +424 -0
  9. package/mtds/ai/avatar.mdx +45 -0
  10. package/mtds/ai/avatar.stories.tsx +109 -0
  11. package/mtds/ai/badge.mdx +70 -0
  12. package/mtds/ai/badge.stories.tsx +122 -0
  13. package/mtds/ai/breadcrumbs.mdx +36 -0
  14. package/mtds/ai/breadcrumbs.stories.tsx +158 -0
  15. package/mtds/ai/button.mdx +179 -0
  16. package/mtds/ai/button.stories.tsx +440 -0
  17. package/mtds/ai/card.mdx +51 -0
  18. package/mtds/ai/card.stories.tsx +469 -0
  19. package/mtds/ai/chart.mdx +67 -0
  20. package/mtds/ai/chart.stories.tsx +519 -0
  21. package/mtds/ai/chip.mdx +71 -0
  22. package/mtds/ai/chip.stories.tsx +211 -0
  23. package/mtds/ai/details.mdx +33 -0
  24. package/mtds/ai/details.stories.tsx +91 -0
  25. package/mtds/ai/dialog.mdx +38 -0
  26. package/mtds/ai/dialog.stories.tsx +373 -0
  27. package/mtds/ai/divider.mdx +19 -0
  28. package/mtds/ai/divider.stories.tsx +50 -0
  29. package/mtds/ai/errorsummary.mdx +26 -0
  30. package/mtds/ai/errorsummary.stories.tsx +137 -0
  31. package/mtds/ai/field.mdx +86 -0
  32. package/mtds/ai/field.stories.tsx +863 -0
  33. package/mtds/ai/fieldset.mdx +126 -0
  34. package/mtds/ai/fieldset.stories.tsx +298 -0
  35. package/mtds/ai/fileupload.mdx +16 -0
  36. package/mtds/ai/fileupload.stories.tsx +126 -0
  37. package/mtds/ai/helptext.mdx +24 -0
  38. package/mtds/ai/helptext.stories.tsx +106 -0
  39. package/mtds/ai/input.mdx +223 -0
  40. package/mtds/ai/input.stories.tsx +352 -0
  41. package/mtds/ai/law.mdx +115 -0
  42. package/mtds/ai/law.stories.tsx +168 -0
  43. package/mtds/ai/layout.mdx +145 -0
  44. package/mtds/ai/layout.stories.tsx +443 -0
  45. package/mtds/ai/link.mdx +45 -0
  46. package/mtds/ai/link.stories.tsx +44 -0
  47. package/mtds/ai/logo.mdx +86 -0
  48. package/mtds/ai/logo.stories.tsx +146 -0
  49. package/mtds/ai/pagination.mdx +136 -0
  50. package/mtds/ai/pagination.stories.tsx +404 -0
  51. package/mtds/ai/popover.mdx +86 -0
  52. package/mtds/ai/popover.stories.tsx +355 -0
  53. package/mtds/ai/print.mdx +96 -0
  54. package/mtds/ai/print.stories.tsx +839 -0
  55. package/mtds/ai/progress.mdx +41 -0
  56. package/mtds/ai/progress.stories.tsx +141 -0
  57. package/mtds/ai/skeleton.mdx +26 -0
  58. package/mtds/ai/skeleton.stories.tsx +131 -0
  59. package/mtds/ai/spinner.mdx +26 -0
  60. package/mtds/ai/spinner.stories.tsx +72 -0
  61. package/mtds/ai/steps.mdx +37 -0
  62. package/mtds/ai/steps.stories.tsx +568 -0
  63. package/mtds/ai/table.mdx +124 -0
  64. package/mtds/ai/table.stories.tsx +1715 -0
  65. package/mtds/ai/tabs.mdx +106 -0
  66. package/mtds/ai/tabs.stories.tsx +159 -0
  67. package/mtds/ai/tag.mdx +49 -0
  68. package/mtds/ai/tag.stories.tsx +111 -0
  69. package/mtds/ai/toast.mdx +67 -0
  70. package/mtds/ai/toast.stories.tsx +215 -0
  71. package/mtds/ai/togglegroup.mdx +75 -0
  72. package/mtds/ai/togglegroup.stories.tsx +96 -0
  73. package/mtds/ai/tooltip.mdx +32 -0
  74. package/mtds/ai/tooltip.stories.tsx +34 -0
  75. package/mtds/ai/typography.mdx +67 -0
  76. package/mtds/ai/typography.stories.tsx +798 -0
  77. package/mtds/ai/validation.mdx +19 -0
  78. package/mtds/ai/validation.stories.tsx +45 -0
  79. package/mtds/atlas/atlas-element.js +1 -1
  80. package/mtds/chart/chart-lines.js +19 -19
  81. package/mtds/chart/chart-lines.js.map +1 -1
  82. package/mtds/chart/chart.css.js +16 -1
  83. package/mtds/chart/chart.css.js.map +1 -1
  84. package/mtds/chart/chart.stories.d.ts +1 -0
  85. package/mtds/index.iife.js +32 -17
  86. package/mtds/package.json.js +1 -1
  87. package/mtds/styles.css +1 -1
  88. package/mtds/table/table-observer.js +26 -15
  89. package/mtds/table/table-observer.js.map +1 -1
  90. package/package.json +4 -2
@@ -0,0 +1,863 @@
1
+ import { PintGlassIcon, WindIcon } from "@phosphor-icons/react";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
+ import { useRef, useState } from "react";
4
+ import type { DSSuggestionElement } from "../";
5
+ import type { FieldSuggestionSelected } from "../react";
6
+ import { Field, Flex, Input } from "../react";
7
+ import styles from "../styles.module.css";
8
+
9
+ const meta = {
10
+ title: "Designsystem/Field",
11
+ argTypes: {
12
+ as: {
13
+ description: "Element type",
14
+ table: {
15
+ defaultValue: { summary: "input" },
16
+ type: { summary: "input | textarea | select" },
17
+ },
18
+ },
19
+ type: {
20
+ description: "Input type",
21
+ table: {
22
+ type: { summary: "text | checkbox | radio | ..." },
23
+ },
24
+ },
25
+ label: {
26
+ description: "Label text",
27
+ table: {
28
+ type: { summary: "React.ReactNode" },
29
+ },
30
+ },
31
+ description: {
32
+ description: "Description text",
33
+ table: {
34
+ type: { summary: "React.ReactNode" },
35
+ },
36
+ },
37
+ value: {
38
+ description: "Value",
39
+ table: {
40
+ type: { summary: "string" },
41
+ },
42
+ },
43
+ checked: {
44
+ description: 'If `type="checkbox"` or `type="radio"`',
45
+ table: {
46
+ type: { summary: "boolean" },
47
+ },
48
+ },
49
+ validation: {
50
+ description: "Validation message",
51
+ table: {
52
+ type: { summary: "React.ReactNode" },
53
+ },
54
+ },
55
+ count: {
56
+ description: "Character count",
57
+ table: {
58
+ type: { summary: "number" },
59
+ },
60
+ },
61
+ options: {
62
+ description: 'If `as="select"` or `as={Field.Suggestion}`',
63
+ table: {
64
+ type: {
65
+ summary:
66
+ "string[] | { label: string; value: string; children?: React.ReactNode }[]",
67
+ },
68
+ },
69
+ },
70
+ prefix: {
71
+ description: "Prefix",
72
+ table: {
73
+ type: { summary: "string" },
74
+ },
75
+ },
76
+ suffix: {
77
+ description: "Suffix",
78
+ table: {
79
+ type: { summary: "string" },
80
+ },
81
+ },
82
+ helpText: {
83
+ description: "What to display in HelpText",
84
+ table: {
85
+ type: { summary: "React.ReactNode" },
86
+ },
87
+ },
88
+ helpTextLabel: {
89
+ description: "Label of HelpText button",
90
+ table: {
91
+ type: { summary: "string" },
92
+ },
93
+ },
94
+ "data-nofilter": {
95
+ description: "If `as={Field.Suggestion}`",
96
+ table: {
97
+ type: { summary: "boolean" },
98
+ },
99
+ },
100
+ selected: {
101
+ description: "If `as={Field.Suggestion}`",
102
+ table: {
103
+ type: {
104
+ summary:
105
+ "{ label: string; value: string; children?: React.ReactNode }[]",
106
+ },
107
+ },
108
+ },
109
+ onSelectedChange: {
110
+ description: "If `as={Field.Suggestion}`",
111
+ table: {
112
+ type: { summary: "(selected) => void" },
113
+ },
114
+ },
115
+ },
116
+ decorators: [
117
+ (Story) => (
118
+ <div className={styles.grid}>
119
+ <Story />
120
+ </div>
121
+ ),
122
+ ],
123
+ } satisfies Meta;
124
+
125
+ export default meta;
126
+ type Story = StoryObj<typeof meta>;
127
+
128
+ export const Default: Story = {
129
+ render: () => (
130
+ <ds-field className={styles.field}>
131
+ <label>Ledetekst</label>
132
+ <p data-field="description">Beskrivelse</p>
133
+ <input type="text" className={styles.input} />
134
+ </ds-field>
135
+ ),
136
+ };
137
+
138
+ export const React: Story = {
139
+ render: () => (
140
+ <>
141
+ <h2>
142
+ Field uten <code>as</code> attributt lar deg bygge opp av bestanddeler:
143
+ </h2>
144
+ <Field>
145
+ <Field.Label>Ledetekst</Field.Label>
146
+ <Field.Description>Beskrivelse</Field.Description>
147
+ <Input />
148
+ </Field>
149
+ <br />
150
+ <h2>
151
+ Field med <code>as="input"</code> gir deg en komplett field med
152
+ <small></small>
153
+ <br />
154
+ <code>label</code> + <code>description</code> + <code>input</code> +{" "}
155
+ <code>error</code> + <code>prefix</code> + <code>suffix</code>:
156
+ </h2>
157
+ <Field
158
+ as="input"
159
+ label="Ledetekst"
160
+ helpText="Hjelpetekst"
161
+ helpTextLabel="Vis hjelpetekst"
162
+ description="Beskrivelse"
163
+ validation="Feilmelding"
164
+ prefix="Før"
165
+ suffix="Etter"
166
+ />
167
+ <br />
168
+ <h2>
169
+ Field med <code>as="input"</code> og <code>type="checkbox"</code>:
170
+ </h2>
171
+ <Field as="input" type="checkbox" label="Ledetekst" />
172
+ <br />
173
+ <h2>
174
+ Field med <code>as="textarea"</code> og{" "}
175
+ <code>count=&#x7B;15&#x7D;</code>:
176
+ </h2>
177
+ <Field
178
+ as="textarea"
179
+ label="Ledetekst"
180
+ description="Beskrivelse"
181
+ count={15}
182
+ />
183
+ <br />
184
+ <h2>
185
+ Field med <code>as="select"</code>:
186
+ </h2>
187
+ <Field as="select" label="Ledetekst" options={["Option 1", "Option 2"]} />
188
+ </>
189
+ ),
190
+ };
191
+
192
+ export const Required: Story = {
193
+ parameters: { showInOverview: true },
194
+ render: () => (
195
+ <ds-field className={styles.field}>
196
+ <label>Ledetekst</label>
197
+ <p data-field="description">Beskrivelse</p>
198
+ <input type="text" required className={styles.input} />
199
+ </ds-field>
200
+ ),
201
+ };
202
+
203
+ export const Indeterminate: Story = {
204
+ render: function Render() {
205
+ const [checked, setChecked] = useState(["1", "2"]);
206
+ const all = ["1", "2", "3"];
207
+ const isAll = checked.length === all.length;
208
+
209
+ const onChange = ({ target }: React.ChangeEvent<HTMLInputElement>) =>
210
+ setChecked(
211
+ checked.includes(target.value)
212
+ ? checked.filter((v) => v !== target.value)
213
+ : [...checked, target.value],
214
+ );
215
+
216
+ return (
217
+ <fieldset className={styles.fieldset}>
218
+ <legend>Velg alternativer</legend>
219
+ <ds-field className={styles.field}>
220
+ <label>Velg alle</label>
221
+ <input
222
+ checked={isAll}
223
+ className={styles.input}
224
+ data-indeterminate={!isAll && !!checked.length}
225
+ onChange={() => setChecked(isAll ? [] : all)}
226
+ type="checkbox"
227
+ />
228
+ </ds-field>
229
+ <div className={styles.grid} style={{ paddingLeft: "var(--mtds-8)" }}>
230
+ {all.map((value) => (
231
+ <ds-field className={styles.field} key={value}>
232
+ <label>Alternativ 1</label>
233
+ <input
234
+ checked={checked.includes(value)}
235
+ className={styles.input}
236
+ onChange={onChange}
237
+ type="checkbox"
238
+ value={value}
239
+ />
240
+ </ds-field>
241
+ ))}
242
+ </div>
243
+ </fieldset>
244
+ );
245
+ },
246
+ };
247
+
248
+ export const Toggles: Story = {
249
+ render: () => (
250
+ <>
251
+ <fieldset className={styles.fieldset} aria-label="Velg alternativ">
252
+ <ds-field className={styles.field}>
253
+ <label>Radio 1</label>
254
+ <input
255
+ type="radio"
256
+ className={styles.input}
257
+ name="my-radio"
258
+ defaultChecked
259
+ />
260
+ </ds-field>
261
+ <ds-field className={styles.field}>
262
+ <label>Radio 2</label>
263
+ <p data-field="description">Beskrivelse</p>
264
+ <input type="radio" className={styles.input} name="my-radio" />
265
+ </ds-field>
266
+ </fieldset>
267
+ <ds-field className={styles.field}>
268
+ <label>Check</label>
269
+ <input type="checkbox" className={styles.input} />
270
+ </ds-field>
271
+ <ds-field className={styles.field}>
272
+ <label>Switch</label>
273
+ <input type="checkbox" className={styles.input} role="switch" />
274
+ </ds-field>
275
+ </>
276
+ ),
277
+ };
278
+
279
+ export const WithValidation: Story = {
280
+ parameters: { showInOverview: true },
281
+ render: () => (
282
+ <ds-field className={styles.field}>
283
+ <label>Ledetekst</label>
284
+ <p data-field="description">Beskrivelse</p>
285
+ <input type="text" className={styles.input} />
286
+ <div className={styles.validation} data-field="validation">
287
+ Validation
288
+ </div>
289
+ </ds-field>
290
+ ),
291
+ };
292
+
293
+ export const WithValidationForm: Story = {
294
+ parameters: { layout: "padded" },
295
+ name: "With Validation Form",
296
+ render: () => {
297
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([]);
298
+
299
+ return (
300
+ <form action="#" className={styles.prose} data-validation="form">
301
+ <ds-field className={styles.field}>
302
+ <label>E-post</label>
303
+ <p data-field="description">Beskrivelse</p>
304
+ <input type="email" className={styles.input} required />
305
+ <div className={styles.validation} data-field="validation">
306
+ Må inneholde en gyldig e-postadresse
307
+ </div>
308
+ </ds-field>
309
+ <ds-field className={styles.field}>
310
+ <label>Tekst</label>
311
+ <p data-field="description">Beskrivelse</p>
312
+ <input type="text" className={styles.input} required />
313
+ <div className={styles.validation} data-field="validation">
314
+ Må fylles ut
315
+ </div>
316
+ </ds-field>
317
+ <ds-field className={styles.field}>
318
+ <label>Suggestion</label>
319
+ <ds-suggestion className={styles.suggestion}>
320
+ <select hidden></select>
321
+ <input type="text" className={styles.input} aria-required="true" />
322
+ <del role="img" aria-label="Fjern tekst"></del>
323
+ <u-datalist>
324
+ <u-option value="Sogndal">Sogndal</u-option>
325
+ <u-option value="Oslo">Oslo</u-option>
326
+ <u-option value="Brønnøysund">Brønnøysund</u-option>
327
+ <u-option value="Stavanger">Stavanger</u-option>
328
+ <u-option value="Trondheim">Trondheim</u-option>
329
+ <u-option value="Bergen">Bergen</u-option>
330
+ <u-option value="Lillestrøm">Lillestrøm</u-option>
331
+ </u-datalist>
332
+ </ds-suggestion>
333
+ <div className={styles.validation} data-field="validation">
334
+ Må fylles ut
335
+ </div>
336
+ </ds-field>
337
+ <Field
338
+ as="input"
339
+ validation="Må fylles ut"
340
+ label="React input"
341
+ required
342
+ />
343
+ <Field
344
+ aria-required="true"
345
+ as={Field.Suggestion}
346
+ label="React Suggestion"
347
+ onSelectedChange={setSelected}
348
+ selected={selected}
349
+ validation="Må fylles ut"
350
+ options={[
351
+ { value: "Sogndal", label: "Sogndal" },
352
+ { value: "Oslo", label: "Oslo" },
353
+ { value: "Bergen", label: "Bergen" },
354
+ ]}
355
+ />
356
+ <button type="submit" className={styles.button} data-variant="primary">
357
+ Send inn
358
+ </button>
359
+ </form>
360
+ );
361
+ },
362
+ };
363
+
364
+ export const WithAffixes: Story = {
365
+ parameters: {
366
+ showInOverview: true,
367
+ },
368
+ render: () => (
369
+ <ds-field className={styles.field}>
370
+ <label>Pris i NOK per måned</label>
371
+ <div className={styles.affixes}>
372
+ <span>NOK</span>
373
+ <input type="text" className={styles.input} />
374
+ <span>pr. mnd.</span>
375
+ </div>
376
+ </ds-field>
377
+ ),
378
+ };
379
+
380
+ export const WithCharacterCount: Story = {
381
+ parameters: {
382
+ layout: "padded",
383
+ showInOverview: true,
384
+ },
385
+ render: () => (
386
+ <ds-field className={styles.field}>
387
+ <label>Ledetekst</label>
388
+ <p data-field="description">Beskrivelse</p>
389
+ <textarea className={styles.input} defaultValue="Noe innhold" />
390
+ <p data-field="counter" data-limit="20" />
391
+ </ds-field>
392
+ ),
393
+ };
394
+
395
+ export const WithCustomDescriptionTag: Story = {
396
+ parameters: { layout: "padded" },
397
+ render: () => (
398
+ <ds-field className={styles.field}>
399
+ <label>Ledetekst</label>
400
+ <div data-field="description" className={styles.prose}>
401
+ <p data-field="description">
402
+ Beskrivelse i en <code>data-field="description"</code> som virker med
403
+ og uten <code>&lt;p&gt;</code>
404
+ </p>
405
+ </div>
406
+ <textarea className={styles.input} defaultValue="Noe innhold" />
407
+ <p className={styles.validation} data-field="counter" data-limit="20" />
408
+ </ds-field>
409
+ ),
410
+ };
411
+
412
+ export const WithSuggestion: Story = {
413
+ parameters: {
414
+ layout: "padded",
415
+ },
416
+ render: () => (
417
+ <ds-field className={styles.field}>
418
+ <label>Med forslag</label>
419
+ <ds-suggestion className={styles.suggestion}>
420
+ <input type="text" className={styles.input} />
421
+ <del role="img" aria-label="Fjern tekst"></del>
422
+ <u-datalist>
423
+ <u-option value="Sogndal">Sogndal</u-option>
424
+ <u-option value="Oslo">Oslo</u-option>
425
+ <u-option value="Brønnøysund">Brønnøysund</u-option>
426
+ <u-option value="Stavanger">Stavanger</u-option>
427
+ <u-option value="Trondheim">Trondheim</u-option>
428
+ <u-option value="Bergen">Bergen</u-option>
429
+ <u-option value="Lillestrøm">Lillestrøm</u-option>
430
+ </u-datalist>
431
+ </ds-suggestion>
432
+ <select name="form-data" hidden></select>
433
+ </ds-field>
434
+ ),
435
+ };
436
+
437
+ export const WithSuggestionMultiple: Story = {
438
+ parameters: {
439
+ layout: "padded",
440
+ },
441
+ render: () => (
442
+ <ds-field className={styles.field}>
443
+ <label>Med forslag flervalg</label>
444
+ <ds-suggestion className={styles.suggestion} data-multiple>
445
+ <data value="Sogndal">Sogndal</data>
446
+ <input type="text" className={styles.input} />
447
+ <del role="img" aria-label="Fjern tekst"></del>
448
+ <u-datalist>
449
+ <u-option value="Sogndal">Sogndal</u-option>
450
+ <u-option value="Oslo">Oslo</u-option>
451
+ <u-option value="Brønnøysund">Brønnøysund</u-option>
452
+ <u-option value="Stavanger">Stavanger</u-option>
453
+ <u-option value="Trondheim">Trondheim</u-option>
454
+ <u-option value="Bergen">Bergen</u-option>
455
+ <u-option value="Lillestrøm">Lillestrøm</u-option>
456
+ </u-datalist>
457
+ </ds-suggestion>
458
+ </ds-field>
459
+ ),
460
+ };
461
+
462
+ export const WithSuggestionMultipleInside: Story = {
463
+ parameters: {
464
+ layout: "padded",
465
+ },
466
+ render: () => (
467
+ <ds-field className={styles.field}>
468
+ <label>Med forslag flervalg inni</label>
469
+ <ds-suggestion
470
+ className={styles.suggestion}
471
+ data-multiple
472
+ data-variant="inside"
473
+ >
474
+ <data value="Sogndal">Sogndal</data>
475
+ <input type="text" className={styles.input} />
476
+ <del role="img" aria-label="Fjern tekst"></del>
477
+ <u-datalist>
478
+ <u-option value="Sogndal">Sogndal</u-option>
479
+ <u-option value="Oslo">Oslo</u-option>
480
+ <u-option value="Brønnøysund">Brønnøysund</u-option>
481
+ <u-option value="Stavanger">Stavanger</u-option>
482
+ <u-option value="Trondheim">Trondheim</u-option>
483
+ <u-option value="Bergen">Bergen</u-option>
484
+ <u-option value="Lillestrøm">Lillestrøm</u-option>
485
+ </u-datalist>
486
+ </ds-suggestion>
487
+ </ds-field>
488
+ ),
489
+ };
490
+
491
+ export const WithSuggestionCreatable: Story = {
492
+ parameters: {
493
+ layout: "padded",
494
+ },
495
+ render: () => (
496
+ <ds-field className={styles.field}>
497
+ <label>Skriv noe som ikke finnes i listen og trykk Enter:</label>
498
+ <ds-suggestion className={styles.suggestion} data-multiple data-creatable>
499
+ <data value="Sogndal">Sogndal</data>
500
+ <input type="text" className={styles.input} />
501
+ <del role="img" aria-label="Fjern tekst"></del>
502
+ <u-datalist>
503
+ <u-option value="Sogndal">Sogndal</u-option>
504
+ <u-option value="Oslo">Oslo</u-option>
505
+ <u-option value="Brønnøysund">Brønnøysund</u-option>
506
+ <u-option value="Stavanger">Stavanger</u-option>
507
+ <u-option value="Trondheim">Trondheim</u-option>
508
+ <u-option value="Bergen">Bergen</u-option>
509
+ <u-option value="Lillestrøm">Lillestrøm</u-option>
510
+ </u-datalist>
511
+ </ds-suggestion>
512
+ </ds-field>
513
+ ),
514
+ };
515
+
516
+ export const WithSuggestionAPI: Story = {
517
+ parameters: {
518
+ layout: "padded",
519
+ },
520
+ render: () => {
521
+ const [options, setOptions] = useState<string[] | string>("Name a country"); // Store results
522
+ const timer = useRef<ReturnType<typeof setTimeout> | number>(0);
523
+
524
+ const getCountries = async (value: string) => {
525
+ if (!value) return setOptions("Name a country");
526
+ const api = `https://restcountries.com/v2/name/${value}?fields=name`;
527
+ const countries = await (await fetch(api)).json();
528
+
529
+ setOptions(
530
+ Array.isArray(countries)
531
+ ? countries.map(({ name }) => name)
532
+ : "No results",
533
+ );
534
+ };
535
+
536
+ const handleInput = (event: React.InputEvent<HTMLInputElement>) => {
537
+ const value = encodeURIComponent(event.currentTarget.value.trim());
538
+
539
+ setOptions(value ? "Loading..." : "Name a country");
540
+ clearTimeout(timer.current);
541
+ timer.current = setTimeout(getCountries, 500, value); // Debounce API call
542
+ };
543
+
544
+ return (
545
+ <ds-field className={styles.field}>
546
+ <label>Med henting av resultater fra API</label>
547
+ <ds-suggestion className={styles.suggestion}>
548
+ <input
549
+ type="search"
550
+ className={styles.input}
551
+ onInput={handleInput} // Note: using onInput, not onChange
552
+ />
553
+ <del role="img" aria-label="Fjern tekst"></del>
554
+ <u-datalist data-nofilter>
555
+ {Array.isArray(options) ? (
556
+ options.map((option) => (
557
+ <u-option key={option}>{option}</u-option>
558
+ ))
559
+ ) : (
560
+ <u-option value="">{options}</u-option>
561
+ )}
562
+ </u-datalist>
563
+ </ds-suggestion>
564
+ </ds-field>
565
+ );
566
+ },
567
+ };
568
+
569
+ export const WithSuggestionCustomFilter: Story = {
570
+ parameters: {
571
+ layout: "padded",
572
+ },
573
+ render: () => {
574
+ const ref = useRef<DSSuggestionElement>(null);
575
+ const [value, setValue] = useState("");
576
+ const options = [
577
+ "Sogndal",
578
+ "Oslo",
579
+ "Brønnøysund",
580
+ "Stavanger",
581
+ "Trondheim",
582
+ "Bergen",
583
+ "Lillestrøm",
584
+ ];
585
+
586
+ return (
587
+ <ds-field className={styles.field}>
588
+ <label>Eget filter - gir kun treff fra starten av ordet</label>
589
+ <ds-suggestion className={styles.suggestion} ref={ref}>
590
+ <input
591
+ className={styles.input}
592
+ onInput={({ currentTarget }) => setValue(currentTarget.value)}
593
+ />
594
+ <del role="img" aria-label="Fjern tekst"></del>
595
+ <u-datalist data-nofilter>
596
+ {options
597
+ .filter((option) =>
598
+ option.toLowerCase().startsWith(value.toLowerCase()),
599
+ )
600
+ .map((option) => (
601
+ <u-option key={option} value={option}>
602
+ {option}
603
+ </u-option>
604
+ ))}
605
+ </u-datalist>
606
+ </ds-suggestion>
607
+ </ds-field>
608
+ );
609
+ },
610
+ };
611
+
612
+ export const ReactWithSuggestion: Story = {
613
+ parameters: {
614
+ layout: "padded",
615
+ },
616
+ render: function Render() {
617
+ // IMPORTANT:
618
+ // Using Field.Suggestion requires
619
+ // "use client" if doing server-side rendering
620
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([
621
+ { value: "saft", label: "Saft" },
622
+ ]);
623
+
624
+ return (
625
+ <Field
626
+ as={Field.Suggestion}
627
+ label="React med forslag"
628
+ description="Beskrivelse"
629
+ selected={selected}
630
+ onSelectedChange={setSelected}
631
+ options={[
632
+ { value: "saft", label: "Saft" },
633
+ {
634
+ value: "suse",
635
+ label: "Suse",
636
+ children: (
637
+ <Flex data-align="center">
638
+ <WindIcon /> Suse
639
+ </Flex>
640
+ ),
641
+ },
642
+ ]}
643
+ />
644
+ );
645
+ },
646
+ };
647
+
648
+ export const ReactWithSuggestionWithChildren: Story = {
649
+ parameters: {
650
+ layout: "padded",
651
+ },
652
+ render: function Render() {
653
+ const multiple = true;
654
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([]);
655
+
656
+ return (
657
+ <Field>
658
+ <Field.Label>React med Field.Suggestion med barn</Field.Label>
659
+ <Field.Description>
660
+ Hvis Field.Suggestion har barn, tegner den ikke input selv.
661
+ </Field.Description>
662
+ <Field.Suggestion
663
+ data-multiple={multiple}
664
+ selected={selected}
665
+ onSelectedChange={setSelected}
666
+ >
667
+ <Input />
668
+ <del role="img" aria-label="Fjern tekst"></del>
669
+ <Field.Datalist>
670
+ <Field.Option value="saft" label="Saft">
671
+ <Flex data-align="center">
672
+ <PintGlassIcon /> Saft
673
+ </Flex>
674
+ </Field.Option>
675
+ <Field.Option value="suse" label="Suse">
676
+ <Flex data-align="center">
677
+ <WindIcon /> Suse
678
+ </Flex>
679
+ </Field.Option>
680
+ </Field.Datalist>
681
+ </Field.Suggestion>
682
+ </Field>
683
+ );
684
+ },
685
+ };
686
+
687
+ export const ReactWithSuggestionMultiple: Story = {
688
+ parameters: {
689
+ layout: "padded",
690
+ },
691
+ render: function Render() {
692
+ // IMPORTANT:
693
+ // Using Field.Suggestion requires
694
+ // "use client" if doing server-side rendering
695
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([
696
+ { value: "saft", label: "Saft" },
697
+ ]);
698
+
699
+ return (
700
+ <Field
701
+ as={Field.Suggestion}
702
+ data-multiple
703
+ label="React med forslag flervalg"
704
+ selected={selected}
705
+ onSelectedChange={setSelected}
706
+ options={[
707
+ { value: "saft", label: "Saft" },
708
+ { value: "suse", label: "Suse" },
709
+ ]}
710
+ />
711
+ );
712
+ },
713
+ };
714
+
715
+ export const ReactWithSuggestionMultipleInside: Story = {
716
+ parameters: {
717
+ layout: "padded",
718
+ },
719
+ render: function Render() {
720
+ // IMPORTANT:
721
+ // Using Field.Suggestion requires
722
+ // "use client" if doing server-side rendering
723
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([
724
+ { value: "saft", label: "Saft" },
725
+ ]);
726
+
727
+ return (
728
+ <Field
729
+ as={Field.Suggestion}
730
+ data-multiple
731
+ data-variant="inside"
732
+ label="React med forslag flervalg"
733
+ selected={selected}
734
+ onSelectedChange={setSelected}
735
+ options={[
736
+ { value: "saft", label: "Saft" },
737
+ { value: "suse", label: "Suse" },
738
+ ]}
739
+ />
740
+ );
741
+ },
742
+ };
743
+
744
+ export const ReactWithSuggestionCreatable: Story = {
745
+ parameters: {
746
+ layout: "padded",
747
+ },
748
+ render: function Render() {
749
+ // IMPORTANT:
750
+ // Using Field.Suggestion requires
751
+ // "use client" if doing server-side rendering
752
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([
753
+ { value: "saft", label: "Saft" },
754
+ ]);
755
+
756
+ return (
757
+ <Field
758
+ as={Field.Suggestion}
759
+ data-multiple
760
+ data-creatable
761
+ label="React med forslag flervalg"
762
+ selected={selected}
763
+ onSelectedChange={setSelected}
764
+ options={[
765
+ { value: "saft", label: "Saft" },
766
+ { value: "suse", label: "Suse" },
767
+ ]}
768
+ />
769
+ );
770
+ },
771
+ };
772
+
773
+ export const ReactWithSuggestionLong: Story = {
774
+ parameters: {
775
+ layout: "padded",
776
+ },
777
+ render: () => {
778
+ // IMPORTANT:
779
+ // Using Field.Suggestion requires
780
+ // "use client" if doing server-side rendering
781
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([]);
782
+
783
+ return (
784
+ <>
785
+ <div style={{ height: 400 }} />
786
+ <Field>
787
+ <Field.Label>React med lange og mange</Field.Label>
788
+ <Field.Description>Beskrivelse</Field.Description>
789
+ <Field.Suggestion selected={selected} onSelectedChange={setSelected}>
790
+ <Input />
791
+ <del role="img" aria-label="Fjern tekst"></del>
792
+ <Field.Datalist data-nofilter data-placement="top">
793
+ <Field.Option>
794
+ Thunder Thunder Thunder Thunder Thunder Thunder Thunder Thunder
795
+ Thunder Thunder Thunder Thunder Thunder
796
+ </Field.Option>
797
+ <Field.Option>Whisper</Field.Option>
798
+ <Field.Option>Galaxy</Field.Option>
799
+ <Field.Option>Melody</Field.Option>
800
+ <Field.Option>Crystal</Field.Option>
801
+ <Field.Option>Sunset</Field.Option>
802
+ <Field.Option>Journey</Field.Option>
803
+ <Field.Option>Phoenix</Field.Option>
804
+ <Field.Option>Harmony</Field.Option>
805
+ <Field.Option>Neptune</Field.Option>
806
+ <Field.Option>Cascade</Field.Option>
807
+ <Field.Option>Velvet</Field.Option>
808
+ <Field.Option>Rhythm</Field.Option>
809
+ <Field.Option>Compass</Field.Option>
810
+ <Field.Option>Prism</Field.Option>
811
+ <Field.Option>Breeze</Field.Option>
812
+ <Field.Option>Eclipse</Field.Option>
813
+ <Field.Option>Sterling</Field.Option>
814
+ <Field.Option>Canvas</Field.Option>
815
+ <Field.Option>Zenith</Field.Option>
816
+ </Field.Datalist>
817
+ </Field.Suggestion>
818
+ </Field>
819
+ <div style={{ height: 800 }} />
820
+ </>
821
+ );
822
+ },
823
+ };
824
+
825
+ export const ReactWithSuggestionCustomFilter: Story = {
826
+ parameters: {
827
+ layout: "padded",
828
+ },
829
+ render: function Render() {
830
+ const [value, setValue] = useState("");
831
+ const [selected, setSelected] = useState<FieldSuggestionSelected>([]);
832
+ const options = [
833
+ "Sogndal",
834
+ "Oslo",
835
+ "Brønnøysund",
836
+ "Stavanger",
837
+ "Trondheim",
838
+ "Bergen",
839
+ "Lillestrøm",
840
+ ];
841
+
842
+ return (
843
+ <Field>
844
+ <Field.Label>Filterer på starten av ordet</Field.Label>
845
+ <Field.Suggestion
846
+ data-nofilter
847
+ data-multiple
848
+ selected={selected}
849
+ onSelectedChange={setSelected}
850
+ options={options
851
+ .filter((opt) => opt.toLowerCase().startsWith(value.toLowerCase()))
852
+ .map((value) => ({ label: value, value }))}
853
+ >
854
+ <Input
855
+ value={value}
856
+ onInput={({ currentTarget }) => setValue(currentTarget.value)}
857
+ />
858
+ <del role="img" aria-label="Fjern tekst"></del>
859
+ </Field.Suggestion>
860
+ </Field>
861
+ );
862
+ },
863
+ };