Skip to content

Commit 9099c6c

Browse files
authored
feat: input (#2550)
- Resolves #2553 - Resolves #2587 - Part of #2311 - Make standalone `<Input>` - Adds support for `type="radio"` and `type="checkbox"` - Adds support for `role="switch"` - Documentation for standalone components should be discussed with the team (#2566) and not solved in this PR - Some changes might occur later after #2561, but I suggest we merge this first to be able to move forward
1 parent 5765d89 commit 9099c6c

File tree

9 files changed

+1201
-0
lines changed

9 files changed

+1201
-0
lines changed

packages/css/index.css

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
@import url('./search.css') layer(ds.components);
2121
@import url('./select.css') layer(ds.components);
2222
@import url('./textfield.css') layer(ds.components);
23+
@import url('./input.css') layer(ds.components);
2324
@import url('./textarea.css') layer(ds.components);
2425
@import url('./helptext.css') layer(ds.components);
2526
@import url('./modal.css') layer(ds.components);

packages/css/input.css

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
.ds-input {
2+
--dsc-input-border-color--checked: var(--ds-color-accent-base-default);
3+
--dsc-input-border-color--invalid: var(--ds-color-danger-border-default);
4+
--dsc-input-border-color--readonly: var(--ds-color-neutral-border-subtle);
5+
--dsc-input-border-color: var(--ds-color-neutral-border-default);
6+
--dsc-input-background--checked: var(--dsc-input-border-color--checked);
7+
--dsc-input-background--invalid: var(--dsc-input-border-color--invalid);
8+
--dsc-input-background--readonly: var(--ds-color-neutral-background-subtle);
9+
--dsc-input-background--switch: var(--ds-color-neutral-border-default);
10+
--dsc-input-background: var(--ds-color-neutral-background-default);
11+
--dsc-input-border-width--toggle: 2px;
12+
--dsc-input-border-width: 1px;
13+
--dsc-input-color--checked: var(--ds-color-accent-contrast-default);
14+
--dsc-input-color--invalid: var(--ds-color-danger-contrast-default);
15+
--dsc-input-color--readonly: var(--ds-color-neutral-border-default);
16+
--dsc-input-color: var(--ds-color-neutral-text-default);
17+
--dsc-input-stroke: 0.05em;
18+
--dsc-input-padding: var(--ds-spacing-2) var(--ds-spacing-3);
19+
--dsc-input-size--switch: var(--ds-sizing-7);
20+
--dsc-input-size--toggle: var(--ds-sizing-6);
21+
--dsc-input-size: var(--ds-sizing-12);
22+
23+
/* Checkmark with antialiasing is achieved by percentages 48% / 50% / 52% */
24+
--check-left: calc(var(--dsc-input-stroke) / 2) calc(66.66% + var(--dsc-input-stroke) / 2) / 33.33% 33.33% no-repeat content-box
25+
linear-gradient(
26+
45deg,
27+
transparent calc(50% - var(--dsc-input-stroke)),
28+
currentcolor calc(48% - var(--dsc-input-stroke)),
29+
currentcolor calc(50% + var(--dsc-input-stroke)),
30+
transparent calc(52% + var(--dsc-input-stroke))
31+
);
32+
--check-right: calc(100% - var(--dsc-input-stroke) / 2) / 66.66% 66.66% no-repeat content-box
33+
linear-gradient(
34+
-45deg,
35+
transparent calc(50% - var(--dsc-input-stroke)),
36+
currentcolor calc(48% - var(--dsc-input-stroke)),
37+
currentcolor calc(50% + var(--dsc-input-stroke)),
38+
transparent calc(52% + var(--dsc-input-stroke))
39+
);
40+
41+
appearance: none;
42+
background: var(--dsc-input-background);
43+
border-radius: var(--ds-border-radius-md);
44+
border: var(--dsc-input-affix-border, var(--dsc-input-border-width) solid var(--dsc-input-border-color)); /* Inherit from .ds-input-addons if present */
45+
box-shadow: var(--dsc-input-box-shadow);
46+
box-sizing: border-box;
47+
color: var(--dsc-input-color);
48+
font-family: inherit;
49+
margin: 0; /* Reset native margin on checkbox and radio */
50+
padding: var(--dsc-input-padding);
51+
position: relative; /* Ensure foucs outline renders on top */
52+
53+
@composes ds-body-text--md from './base/base.css';
54+
@composes ds-focus from './base/base.css';
55+
56+
/* Change switch background with low specificity to allow states to overwrite */
57+
&:where([role='switch']) {
58+
--dsc-input-background: var(--dsc-input-background--switch);
59+
}
60+
61+
&:not(textarea) {
62+
height: var(--dsc-input-size);
63+
}
64+
65+
&:not([size]) {
66+
width: 100%;
67+
}
68+
69+
/**
70+
* States
71+
*/
72+
&:checked,
73+
&:indeterminate {
74+
--dsc-input-border-color: var(--dsc-input-border-color--checked);
75+
--dsc-input-background: var(--dsc-input-background--checked);
76+
--dsc-input-color: var(--dsc-input-color--checked);
77+
}
78+
79+
&:disabled,
80+
&[aria-disabled='true'] {
81+
cursor: not-allowed;
82+
opacity: var(--ds-disabled-opacity);
83+
}
84+
85+
&[aria-invalid='true'] {
86+
--dsc-input-border-color: var(--dsc-input-border-color--invalid);
87+
--dsc-input-background--checked: var(--dsc-input-background--invalid);
88+
--dsc-input-color--checked: var(--dsc-input-color--invalid);
89+
}
90+
91+
/* Using attribute [readonly] since pseudo selector :read-only is always true for checkbox, radio and select */
92+
&[readonly] {
93+
--dsc-input-border-color: var(--dsc-input-border-color--readonly);
94+
--dsc-input-background: var(--dsc-input-background--readonly);
95+
--dsc-input-color: var(--dsc-input-color--readonly);
96+
}
97+
98+
/**
99+
* Sizes
100+
*/
101+
&[data-size='sm'] {
102+
@composes ds-body-text--sm from './base/base.css';
103+
104+
--dsc-input-padding: var(--ds-spacing-1) var(--ds-spacing-2);
105+
--dsc-input-size--switch: var(--ds-sizing-6);
106+
--dsc-input-size--toggle: var(--ds-sizing-5);
107+
--dsc-input-size: var(--ds-sizing-10);
108+
}
109+
110+
&[data-size='lg'] {
111+
@composes ds-body-text--lg from './base/base.css';
112+
113+
--dsc-input-padding: var(--ds-spacing-3) var(--ds-spacing-4);
114+
--dsc-input-size--switch: var(--ds-sizing-8);
115+
--dsc-input-size--toggle: var(--ds-sizing-7);
116+
--dsc-input-size: var(--ds-sizing-14);
117+
}
118+
119+
/**
120+
* Toggle inputs
121+
*/
122+
&:read-only:not([readonly], [aria-disabled='true'], :disabled) {
123+
cursor: pointer;
124+
}
125+
126+
&[type='checkbox'],
127+
&[type='radio'] {
128+
--dsc-input-border-width: var(--dsc-input-border-width--toggle);
129+
--dsc-input-padding: calc(var(--ds-sizing-1) / 2);
130+
--dsc-input-size: var(--dsc-input-size--toggle);
131+
132+
flex-shrink: 0; /* Never shrink a toggle input */
133+
width: var(--dsc-input-size);
134+
}
135+
136+
&[type='radio'] {
137+
border-radius: var(--ds-border-radius-full);
138+
}
139+
140+
&[type='radio']:checked {
141+
background: radial-gradient(circle closest-side, currentcolor 45%, transparent 50%), var(--dsc-input-background);
142+
}
143+
144+
&[type='checkbox']:checked {
145+
background: var(--check-left), var(--check-right), var(--dsc-input-background);
146+
}
147+
148+
&[type='checkbox']:indeterminate {
149+
background: center / contain no-repeat content-box
150+
linear-gradient(
151+
transparent calc(48% - var(--dsc-input-stroke)),
152+
currentcolor calc(50% - var(--dsc-input-stroke)),
153+
currentcolor calc(50% + var(--dsc-input-stroke)),
154+
transparent calc(52% + var(--dsc-input-stroke))
155+
), var(--dsc-input-background);
156+
}
157+
158+
/**
159+
* Switch
160+
*/
161+
&[role='switch']:is([type='radio'], [type='checkbox']) {
162+
--dsc-input-color: transparent; /* Hide checkmark */
163+
--dsc-input-padding: var(--ds-sizing-1);
164+
--dsc-input-size: var(--dsc-input-size--switch);
165+
--circle-color: var(--dsc-input-color--checked);
166+
--circle-position: left;
167+
168+
border-radius: var(--ds-border-radius-full);
169+
padding-left: var(--dsc-input-size); /* Push checkmark to right side */
170+
transition: 0.2s background-position;
171+
width: calc((var(--dsc-input-size) - var(--dsc-input-border-width)) * 2); /* Subtract border-width to make background-image math correct */
172+
background: var(--check-left), var(--check-right), radial-gradient(circle closest-side, var(--circle-color) 95%, transparent 100%) var(--circle-position) /
173+
50% 100% no-repeat padding-box, var(--dsc-input-background);
174+
175+
&:checked {
176+
--dsc-input-color: var(--dsc-input-border-color);
177+
--circle-position: right;
178+
}
179+
180+
&[readonly] {
181+
--circle-color: var(--dsc-input-color--readonly);
182+
}
183+
}
184+
}
185+
186+
/* Change cursor on wrapping <label> */
187+
label:has(input:is([type='checkbox'], [type='radio']):not(:disabled, [aria-disabled], [readonly])) {
188+
cursor: pointer;
189+
}
190+
191+
/**
192+
* Affix
193+
*/
194+
.ds-input-affix {
195+
--dsc-input-affix-border-radius: var(--ds-border-radius-md);
196+
--dsc-input-affix-border: 1px solid var(--ds-color-neutral-border-default);
197+
--dsc-input-affix-padding-inline: var(--ds-spacing-4);
198+
199+
align-items: center;
200+
background: var(--ds-color-neutral-background-subtle);
201+
border-radius: var(--dsc-input-affix-border-radius);
202+
box-sizing: border-box;
203+
color: var(--ds-color-neutral-text-subtle);
204+
display: inline-flex; /* Using inline-flex to match native inline-block behaviour of <input> */
205+
gap: var(--dsc-input-affix-padding-inline);
206+
padding-inline: var(--dsc-input-affix-padding-inline);
207+
position: relative;
208+
white-space: nowrap;
209+
width: fit-content;
210+
211+
/* Using ::before to make input border overlap addons border */
212+
&::before {
213+
border-radius: inherit;
214+
border: var(--dsc-input-affix-border);
215+
content: '';
216+
inset: 0;
217+
pointer-events: none;
218+
position: absolute;
219+
}
220+
221+
@composes ds-body-text--md from './base/base.css';
222+
223+
/* Using double selector to ensure we win specificity */
224+
.ds-input.ds-input {
225+
align-self: stretch;
226+
border-radius: 0;
227+
flex: 1 1 auto;
228+
height: auto;
229+
}
230+
231+
&:has([data-size='sm']) {
232+
@composes ds-body-text--sm from './base/base.css';
233+
234+
--dsc-input-affix-padding-inline: var(--ds-spacing-3);
235+
}
236+
237+
&:has([data-size='lg']) {
238+
@composes ds-body-text--lg from './base/base.css';
239+
240+
--dsc-input-affix-padding-inline: var(--ds-spacing-5);
241+
}
242+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Meta, Canvas, Controls, Primary } from '@storybook/blocks';
2+
3+
import { Alert } from '../../Alert';
4+
import * as InputStories from './Input.stories';
5+
6+
<Meta of={InputStories} />
7+
8+
<Alert color="warning">Input er under utvikling og burde ikke tas i bruk enda. Bruk heller Textfield komponenten.</Alert>
9+
<br />
10+
11+
# Input
12+
13+
`Input` kalles tekstfelt på norsk. Det er et inndatafelt som for eksempel gir brukerne mulighet til å skrive korte tekster eller tall.
14+
15+
<Primary />
16+
<Controls />
17+
18+
## Prefix/Suffix
19+
20+
Prefixer og suffixer er nyttige for å vise enheter, valuta eller andre typer informasjon som er relevant for feltet.
21+
Du skal **ikke** bruke disse alene, siden skjermlesere ikke leser dem opp.
22+
Det er viktig at samme informasjon som vises i prefixet eller suffixet også er inkludert i ledeteksten.
23+
24+
<Canvas of={InputStories.Adornments} />
25+
26+
## Kontrollert
27+
28+
<Canvas of={InputStories.Controlled} />
29+
30+
## Html Size
31+
32+
<Canvas of={InputStories.HtmlSize} />
33+
34+
## Retningslinjer for når du skal bruke `Input`
35+
36+
Vi bruker tekstfelt når vi vil gi brukeren mulighet til å skrive tekst/svar på maks. én linje, Det kan for eksempel være navn eller telefonnummer.
37+
38+
Passer til å
39+
40+
- gi mulighet for korte tekster eller svar
41+
- legge inn tall, for eksempel et telefonnummer
42+
43+
Passer ikke til å
44+
45+
- gi lengre svar, bruk heller `Textarea`
46+
- legge inn formaterte data, som markdown
47+
<br />
48+
49+
### Plassering av ledeteksten
50+
51+
Ledeteksten og en eventuell beskrivelse skal alltid stå over tekstfeltet. Da er de lette å se på små skjermer og hindres ikke av eventuelle feilmeldinger.
52+
53+
### Unngå plassholdertekster
54+
55+
Plassholdertekster forsvinner når brukerne skriver i feltet. Det er derfor bedre å inkludere hint og viktig informasjon i selve ledeteksten eller den tilhørende beskrivelsen.
56+
57+
### Tilpass bredden på tekstfeltet
58+
59+
Tilpass bredden til det brukerne skal skrive inn, kort bredde til telefonnummer og bredere til stedsnavn. Ulik bredde på feltene gjør det enklere å navigere i skjemaer som har mange felter.
60+
61+
### Inndata og formatering
62+
63+
- Bruk `autoComplete` for felter som mottar personlig informasjon. Hvis feltet skal be om personopplysninger om en annen person enn brukeren, må du skru `autoComplete` av (WCAG 1.3.5).
64+
- Bruk gjerne inndatatyper som viser hva du ber om, for eksempel telefonnummer og e-post. Slike inndatatyper gir mobilbrukere et tastatur som passer til det de skal angi, for eksempel et numerisk tastatur for telefonnummer, men de kan også utløse validering på klientsiden.
65+
- Godta det meste av inndata fra brukerne, så lenge det er forståelig. Eksempler kan være kontonummer med punktum, telefonnummer med mellomrom eller mellomrom på slutten av en e-postadresse.
66+
- Pass på at brukerne ser inndata som formateres automatisk, men uten at det forstyrrer dem mens de fyller ut.
67+
- Ikke bruk bare store bokstaver eller kursiv tekst i ledeteksten. Det er vanskelig å lese.
68+
69+
## Tekst i komponenten
70+
71+
Det skal alltid være ledetekst på `Input`. I spesielle tilfeller kan vi skjule ledeteksten med `hidelabel`. Det kan for eksempel være i tabeller, hvis feltet får ledeteksten fra tabelloverskriften. Selv om vi har tenkt å skjule ledeteksten, må vi alltid skrive en ledetekst som gir mening, siden den leses opp av skjermlesere.
72+
73+
## Tilgjengelighet
74+
75+
### Ikke bruk deaktiverte felt
76+
77+
Ikke bruk deaktivert tilstand (disabled state) på tekstfelt. Tenk heller over om du trenger å vise feltet i det hele tatt, eller om du heller kan skrive informasjonen ut i ren tekst eller bruke Read Only.
78+
79+
### Prefiks og suffiks
80+
81+
Prefiks og suffiks er et ekstra visuelt hjelpemiddel, som blir ignorert av skjermlesere. Vi må alltid ha en beskrivende ledetekst. Prefiks og suffiks er plassert utenfor inndatafeltene de tilhører. Da unngår vi at de ikke skaper trøbbel i noen nettlesere, som kan sette inn et ikon i inndatafeltet (for eksempel ikoner for å vise eller lage passord).

0 commit comments

Comments
 (0)