@pokela/components 2.15.0 → 2.16.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.
- package/Dialog.d.ts +34 -0
- package/Dialog.js +33 -0
- package/README.md +3 -1
- package/guidelines/components.md +22 -0
- package/index.d.ts +2 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/styles/dialog.css +125 -0
- package/styles.css +1 -0
package/Dialog.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Modaalinen dialogi: otsikko, teksti, sisältö, toiminnot ja sulkeminen.
|
|
4
|
+
*
|
|
5
|
+
* Sovelluskomponentti (päätökset 22 ja 31). Ei varjoa eikä tummaa
|
|
6
|
+
* verhoa: sivu peittyy paperin sävyisellä verholla, ja dialogi on
|
|
7
|
+
* paperia musteen hiusviivakehyksellä. Uutta väriä ei tarvita, ja
|
|
8
|
+
* sama ratkaisu toimii molemmissa teemoissa. Mobiilissa dialogi on
|
|
9
|
+
* alareunan arkki, ja napit ovat täysleveitä.
|
|
10
|
+
*
|
|
11
|
+
* Natiivi <dialog> ja showModal(): selain pitää fokuksen dialogissa,
|
|
12
|
+
* piilottaa muun sivun ruudunlukijalta ja sulkee Escillä. Esc ja
|
|
13
|
+
* verhon klikkaus kutsuvat `onClose`a, joten tila pysyy kutsujalla.
|
|
14
|
+
*
|
|
15
|
+
* Fokus menee avattaessa ensimmäiseen fokusoitavaan: lomakkeessa
|
|
16
|
+
* ensimmäiseen kenttään, vahvistuksessa Peruuta-nappiin, kun se on
|
|
17
|
+
* toiminnoissa ensimmäisenä. Sulkunappi on DOM:ssa viimeisenä, jotta
|
|
18
|
+
* se ei vie alkufokusta. Suljettaessa fokus palaa avanneeseen nappiin.
|
|
19
|
+
*/
|
|
20
|
+
export type DialogProps = {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
title: string;
|
|
24
|
+
/** Selittävä teksti otsikon alla. Ruudunlukija lukee sen avattaessa. */
|
|
25
|
+
description?: ReactNode;
|
|
26
|
+
/** Sisältö tekstin alla, esim. lomakkeen kentät. */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
/** Napit oikeaan alakulmaan: vähiten vahingollinen ensin. */
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
/** Sulkunapin nimi ruudunlukijalle. */
|
|
31
|
+
closeLabel?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
export default function Dialog({ open, onClose, title, description, children, actions, closeLabel, className, }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
package/Dialog.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef } from 'react';
|
|
4
|
+
import Icon from './Icon';
|
|
5
|
+
export default function Dialog({ open, onClose, title, description, children, actions, closeLabel = 'Sulje', className, }) {
|
|
6
|
+
const ref = useRef(null);
|
|
7
|
+
const avaaja = useRef(null);
|
|
8
|
+
const otsikkoId = useId();
|
|
9
|
+
const tekstiId = useId();
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const el = ref.current;
|
|
12
|
+
if (!el)
|
|
13
|
+
return;
|
|
14
|
+
if (open && !el.open) {
|
|
15
|
+
avaaja.current = document.activeElement;
|
|
16
|
+
el.showModal();
|
|
17
|
+
}
|
|
18
|
+
else if (!open && el.open) {
|
|
19
|
+
el.close();
|
|
20
|
+
avaaja.current?.focus();
|
|
21
|
+
}
|
|
22
|
+
}, [open]);
|
|
23
|
+
return (_jsx("dialog", { ref: ref, className: ['dialog', className].filter(Boolean).join(' '), "aria-labelledby": otsikkoId, "aria-describedby": description ? tekstiId : undefined, onCancel: (e) => {
|
|
24
|
+
/* Esc: selain sulkisi itse, mutta tila on kutsujalla. */
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
onClose();
|
|
27
|
+
}, onClick: (e) => {
|
|
28
|
+
/* Klikkaus osuu itse dialogiin vain verhon kohdalla: sisältö on
|
|
29
|
+
.dialog__panelissa, joka täyttää dialogin. */
|
|
30
|
+
if (e.target === e.currentTarget)
|
|
31
|
+
onClose();
|
|
32
|
+
}, children: _jsxs("div", { className: "dialog__panel", children: [_jsxs("div", { className: "dialog__header", children: [_jsx("h2", { id: otsikkoId, className: "dialog__title", children: title }), description ? (_jsx("div", { id: tekstiId, className: "dialog__description", children: description })) : null] }), children ? _jsx("div", { className: "dialog__body", children: children }) : null, actions ? _jsx("div", { className: "dialog__actions", children: actions }) : null, _jsx("button", { type: "button", className: "dialog__close", "aria-label": closeLabel, onClick: onClose, children: _jsx(Icon, { name: "close" }) })] }) }));
|
|
33
|
+
}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @pokela/components
|
|
2
2
|
|
|
3
|
-
React-komponentit: **Pokela Design System**, versio 2.
|
|
3
|
+
React-komponentit: **Pokela Design System**, versio 2.16.0.
|
|
4
4
|
|
|
5
5
|
> Generoitu hakemistosta `components/` — samoista tiedostoista joita sivusto
|
|
6
6
|
> ajaa. Älä muokkaa käsin: muutokset tehdään lähteeseen ja paketti
|
|
@@ -35,6 +35,7 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
|
|
|
35
35
|
- `Tab`
|
|
36
36
|
- `Alert`
|
|
37
37
|
- `Table`
|
|
38
|
+
- `Dialog`
|
|
38
39
|
- `Grid`
|
|
39
40
|
- `Reveal`
|
|
40
41
|
- `ThemeScript`
|
|
@@ -76,6 +77,7 @@ import Link from 'next/link';
|
|
|
76
77
|
- `styles/tabs.css`
|
|
77
78
|
- `styles/alert.css`
|
|
78
79
|
- `styles/table.css`
|
|
80
|
+
- `styles/dialog.css`
|
|
79
81
|
|
|
80
82
|
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
81
83
|
luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
|
package/guidelines/components.md
CHANGED
|
@@ -20,6 +20,7 @@ Use these instead of building an equivalent.
|
|
|
20
20
|
- `Tab`
|
|
21
21
|
- `Alert`
|
|
22
22
|
- `Table`
|
|
23
|
+
- `Dialog`
|
|
23
24
|
- `Grid`, `Col`
|
|
24
25
|
- `Reveal`
|
|
25
26
|
- `ThemeScript`, `THEME_STORAGE_KEY`
|
|
@@ -184,6 +185,27 @@ row action is a `table__action` link whose name includes the row.
|
|
|
184
185
|
/>
|
|
185
186
|
```
|
|
186
187
|
|
|
188
|
+
`Dialog` is a modal for one decision or a short form: confirm a deletion,
|
|
189
|
+
invite a member. Never use it for content people read, and never stack two.
|
|
190
|
+
Control it with `open` and `onClose`; Esc and a click on the veil call
|
|
191
|
+
`onClose`. Give a short `title` (a question for confirmations), the
|
|
192
|
+
consequence as `description`, form fields as children, and buttons in
|
|
193
|
+
`actions` with the least destructive first (`btn--ghost` Cancel, then
|
|
194
|
+
`btn--primary`). Use `btn--s` buttons. On mobile it becomes a bottom sheet.
|
|
195
|
+
|
|
196
|
+
```jsx
|
|
197
|
+
<Dialog
|
|
198
|
+
open={open}
|
|
199
|
+
onClose={() => setOpen(false)}
|
|
200
|
+
title="Remove Sara from the team?"
|
|
201
|
+
description="Access ends immediately. You can invite them again later."
|
|
202
|
+
actions={<>
|
|
203
|
+
<button className="btn btn--ghost btn--s" onClick={() => setOpen(false)}>Cancel</button>
|
|
204
|
+
<button className="btn btn--primary btn--s" onClick={remove}>Remove member</button>
|
|
205
|
+
</>}
|
|
206
|
+
/>
|
|
207
|
+
```
|
|
208
|
+
|
|
187
209
|
## Patterns
|
|
188
210
|
|
|
189
211
|
`PageHeader` and `Section` are patterns: they add no look of their own, only
|
package/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export { default as Alert } from './Alert.js';
|
|
|
32
32
|
export type { AlertTone, AlertProps } from './Alert.js';
|
|
33
33
|
export { default as Table } from './Table.js';
|
|
34
34
|
export type { TableSortDirection, TableSort, TableColumn, TableProps } from './Table.js';
|
|
35
|
+
export { default as Dialog } from './Dialog.js';
|
|
36
|
+
export type { DialogProps } from './Dialog.js';
|
|
35
37
|
export { Grid, Col } from './Grid.js';
|
|
36
38
|
export { default as Reveal } from './Reveal.js';
|
|
37
39
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { default as Tabs } from './Tabs.js';
|
|
|
17
17
|
export { default as Tab } from './Tab.js';
|
|
18
18
|
export { default as Alert } from './Alert.js';
|
|
19
19
|
export { default as Table } from './Table.js';
|
|
20
|
+
export { default as Dialog } from './Dialog.js';
|
|
20
21
|
export { Grid, Col } from './Grid.js';
|
|
21
22
|
export { default as Reveal } from './Reveal.js';
|
|
22
23
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pokela/components",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "React-komponentit: Pokela Design System. Generoitu tiedostoista components/, ei käsin ylläpidetty.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Veli-Matti Pokela",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"LICENSE"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@pokela/tokens": "^2.
|
|
30
|
+
"@pokela/tokens": "^2.16.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* Dialogi (Dialog). Sovelluskomponentti, sivusto ei käytä tätä.
|
|
2
|
+
==================================================================
|
|
3
|
+
Paperiverho ja hiusviivakehys (päätös 31). Systeemissä ei ole
|
|
4
|
+
varjoja eikä verhoväriä: sivu peittyy paperin sävyisellä verholla,
|
|
5
|
+
ja dialogi erottuu siitä musteviivalla. Verho on läpikuultavaa
|
|
6
|
+
paperia, joten se toimii samoin molemmissa teemoissa.
|
|
7
|
+
|
|
8
|
+
Mobiili ensin: alareunan arkki, jonka yläreunassa on viiva. 600 px
|
|
9
|
+
alkaen keskitetty dialogi, jonka ympärillä on kehys. */
|
|
10
|
+
|
|
11
|
+
.dialog {
|
|
12
|
+
inset: auto 0 0;
|
|
13
|
+
width: 100%;
|
|
14
|
+
max-width: 100%;
|
|
15
|
+
max-height: calc(100dvh - var(--space-32));
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
overscroll-behavior: contain;
|
|
20
|
+
background: var(--paper);
|
|
21
|
+
color: var(--ink);
|
|
22
|
+
border: 0;
|
|
23
|
+
border-top: var(--hairline) solid var(--ink);
|
|
24
|
+
}
|
|
25
|
+
.dialog::backdrop {
|
|
26
|
+
background: color-mix(in srgb, var(--paper) 88%, transparent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Sivu ei vierity dialogin alla. */
|
|
30
|
+
html:has(.dialog[open]) { overflow: hidden; }
|
|
31
|
+
|
|
32
|
+
.dialog__panel {
|
|
33
|
+
position: relative;
|
|
34
|
+
display: grid;
|
|
35
|
+
gap: var(--space-24);
|
|
36
|
+
padding: var(--space-24) var(--space-16) var(--space-32);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.dialog__header {
|
|
40
|
+
display: grid;
|
|
41
|
+
gap: var(--space-8);
|
|
42
|
+
/* Tila sulkunapille, ettei pitkä otsikko mene sen alle. */
|
|
43
|
+
padding-right: var(--space-40);
|
|
44
|
+
}
|
|
45
|
+
.dialog__title {
|
|
46
|
+
margin: 0;
|
|
47
|
+
font-family: var(--font-ui);
|
|
48
|
+
font-size: var(--text-body-l);
|
|
49
|
+
line-height: var(--lh-body-l);
|
|
50
|
+
font-weight: var(--weight-500);
|
|
51
|
+
}
|
|
52
|
+
.dialog__description {
|
|
53
|
+
font-size: var(--text-body-s);
|
|
54
|
+
line-height: var(--lh-body-s);
|
|
55
|
+
color: var(--ink-muted);
|
|
56
|
+
}
|
|
57
|
+
.dialog__description > * { margin: 0; }
|
|
58
|
+
|
|
59
|
+
.dialog__body {
|
|
60
|
+
display: grid;
|
|
61
|
+
gap: var(--space-16);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Mobiilissa napit täysleveinä allekkain, ensisijainen (viimeinen)
|
|
65
|
+
ylimpänä peukalon ulottuvilla. */
|
|
66
|
+
.dialog__actions {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column-reverse;
|
|
69
|
+
gap: var(--space-12);
|
|
70
|
+
}
|
|
71
|
+
.dialog__actions > .btn { width: 100%; }
|
|
72
|
+
|
|
73
|
+
/* Sulkunappi: näkyvä 32 px, osuma-alue --tap-min, kuten Alertissa. */
|
|
74
|
+
.dialog__close {
|
|
75
|
+
position: absolute;
|
|
76
|
+
top: var(--space-16);
|
|
77
|
+
right: var(--space-8);
|
|
78
|
+
display: inline-flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
width: var(--space-32);
|
|
82
|
+
height: var(--space-32);
|
|
83
|
+
padding: 0;
|
|
84
|
+
background: none;
|
|
85
|
+
border: 0;
|
|
86
|
+
color: var(--ink-muted);
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
}
|
|
89
|
+
.dialog__close::after {
|
|
90
|
+
content: '';
|
|
91
|
+
position: absolute;
|
|
92
|
+
inset: calc((var(--space-32) - var(--tap-min)) / 2);
|
|
93
|
+
}
|
|
94
|
+
.dialog__close:hover { color: var(--ink); }
|
|
95
|
+
|
|
96
|
+
@media (min-width: 600px) {
|
|
97
|
+
.dialog {
|
|
98
|
+
inset: 0;
|
|
99
|
+
width: calc(100% - var(--space-32));
|
|
100
|
+
max-width: 480px;
|
|
101
|
+
margin: auto;
|
|
102
|
+
border: var(--hairline) solid var(--ink);
|
|
103
|
+
}
|
|
104
|
+
.dialog__panel { padding: var(--space-32); }
|
|
105
|
+
.dialog__close { top: var(--space-24); right: var(--space-24); }
|
|
106
|
+
.dialog__actions {
|
|
107
|
+
flex-direction: row;
|
|
108
|
+
justify-content: flex-end;
|
|
109
|
+
}
|
|
110
|
+
.dialog__actions > .btn { width: auto; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Liike: dialogi nousee ja verho häivyttää. Ei liikettä, jos käyttäjä
|
|
114
|
+
on sen kieltänyt. */
|
|
115
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
116
|
+
.dialog[open] { animation: dialog-esiin var(--dur-base) var(--ease-out); }
|
|
117
|
+
.dialog[open]::backdrop { animation: dialog-verho var(--dur-base) var(--ease-out); }
|
|
118
|
+
.dialog__close { transition: color var(--dur-fast) var(--ease-standard); }
|
|
119
|
+
}
|
|
120
|
+
@keyframes dialog-esiin { from { opacity: 0; transform: translateY(var(--space-8)); } }
|
|
121
|
+
@keyframes dialog-verho { from { opacity: 0; } }
|
|
122
|
+
|
|
123
|
+
@media (forced-colors: active) {
|
|
124
|
+
.dialog::backdrop { background: Canvas; opacity: 0.88; }
|
|
125
|
+
}
|
package/styles.css
CHANGED