typescript-overlay-essentials 1.0.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/.github/workflows/npm-publish.yml +32 -0
- package/ConfirmationBox.css +69 -0
- package/ConfirmationBox.tsx +148 -0
- package/Dropdown.css +31 -0
- package/Dropdown.tsx +211 -0
- package/InfoOverlay.css +65 -0
- package/InfoOverlay.tsx +101 -0
- package/InfoOverlayWithInput.css +88 -0
- package/InfoOverlayWithInput.tsx +157 -0
- package/InputFilter.tsx +36 -0
- package/LoadingOverlay.css +110 -0
- package/LoadingOverlay.tsx +57 -0
- package/MultipleChoiceOverlay.css +170 -0
- package/MultipleChoiceOverlay.tsx +170 -0
- package/MultipleRadioOverlay.css +170 -0
- package/MultipleRadioOverlay.tsx +158 -0
- package/README.md +12 -0
- package/Toast.css +13 -0
- package/Toast.tsx +34 -0
- package/ToggleSwitch.tsx +56 -0
- package/Toggleswitch.css +49 -0
- package/index.ts +8 -0
- package/package.json +39 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: typescript-overlay-essentials
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
|
+
with:
|
|
17
|
+
node-version: 24
|
|
18
|
+
- run: npm ci
|
|
19
|
+
|
|
20
|
+
publish-npm:
|
|
21
|
+
needs: build
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v5
|
|
25
|
+
- uses: actions/setup-node@v6
|
|
26
|
+
with:
|
|
27
|
+
node-version: 24
|
|
28
|
+
registry-url: https://registry.npmjs.org/
|
|
29
|
+
- run: npm ci
|
|
30
|
+
- run: npm publish
|
|
31
|
+
env:
|
|
32
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
.confirmation-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100vw;
|
|
6
|
+
height: 100vh;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 10000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.confirmation-box {
|
|
15
|
+
background-color: var(--Hellgrau, white);
|
|
16
|
+
color: var(--HellSchwarz,#2c2c2c);
|
|
17
|
+
padding: 2rem;
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
|
|
20
|
+
max-width: 500px;
|
|
21
|
+
width: 90%;
|
|
22
|
+
text-align: center;
|
|
23
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
24
|
+
position: relative;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@keyframes fadeIn {
|
|
28
|
+
from {
|
|
29
|
+
opacity: 0;
|
|
30
|
+
transform: scale(0.95);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
to {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
transform: scale(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.confirmation-box p {
|
|
40
|
+
margin-bottom: 0.5em;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.confirmation-box .headline {
|
|
44
|
+
margin-bottom: 0.75em;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.confirmation-box .confirmation-buttons {
|
|
48
|
+
display: flex;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
gap: 1rem;
|
|
51
|
+
margin-top: 1.5rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.confirmation-box .confirmation-buttons button {
|
|
55
|
+
font-size: 85%;
|
|
56
|
+
padding: 0.5rem 1.1rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.confirmation-box .closeButton {
|
|
60
|
+
right: 10px;
|
|
61
|
+
top: 10px;
|
|
62
|
+
position: absolute;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.confirmation-box .close-icon:hover {
|
|
66
|
+
color: var(--HSDAkzent);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, CSSProperties } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import './ConfirmationBox.css';
|
|
4
|
+
|
|
5
|
+
// Um die ConfirmationBox zu nutzen, muss der State die folgende Struktur haben:
|
|
6
|
+
export interface ConfirmationBoxState {
|
|
7
|
+
headline?: string | undefined;
|
|
8
|
+
message?: string | undefined;
|
|
9
|
+
message1?: string | undefined;
|
|
10
|
+
message2?: string | undefined;
|
|
11
|
+
cancelButtonText?: string | undefined;
|
|
12
|
+
proceedButtonText?: string | undefined;
|
|
13
|
+
handlerOk?: ((args?: unknown) => void) | undefined;
|
|
14
|
+
handlerCancel?: ((args?: unknown) => void) | undefined;
|
|
15
|
+
handlerArgs?: unknown;
|
|
16
|
+
addCloseButton?: boolean | undefined;
|
|
17
|
+
activateConfirm?: boolean | undefined;
|
|
18
|
+
proceedButtonStyle?: CSSProperties | undefined;
|
|
19
|
+
cancelButtonStyle?: CSSProperties | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Der standard ComfirmationBox Status, der zur Initialisierung genutzt werden kann
|
|
23
|
+
export const defaultConfirmationState: ConfirmationBoxState = {
|
|
24
|
+
headline: undefined,
|
|
25
|
+
message: undefined,
|
|
26
|
+
message1: undefined,
|
|
27
|
+
message2: undefined,
|
|
28
|
+
cancelButtonText: undefined,
|
|
29
|
+
proceedButtonText: undefined,
|
|
30
|
+
handlerOk: undefined,
|
|
31
|
+
handlerCancel: undefined,
|
|
32
|
+
handlerArgs: undefined,
|
|
33
|
+
addCloseButton: false,
|
|
34
|
+
activateConfirm: undefined,
|
|
35
|
+
proceedButtonStyle: undefined,
|
|
36
|
+
cancelButtonStyle: undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Wobei alle Attribute grundsätzlich optional sind:
|
|
40
|
+
// - headline: Eine fett hinterlegte Überschrift (optional)
|
|
41
|
+
// - message1: ist die angezeigte Nachricht
|
|
42
|
+
// - message2: ist optional und wird fett hinterlegt (z.B. ein wichtiger Hinweis oder ähnliches)
|
|
43
|
+
// - message: ist optional und wird nur angezeigt, wenn message1 nicht gesetzt ist (damit message oder message1 genutzt werden kann)
|
|
44
|
+
// - cancelButtonText: ist der Text der auf dem Cancel Button stehen soll (ohne Angabe wird "Abbrechen" verwendet)
|
|
45
|
+
// - proceedButtonText: ist der Text der auf dem Proceed Button stehen soll (ohne Angabe wird "OK" verwendet)
|
|
46
|
+
// - handlerOk: ist die Funktion die bei Bestätigung der Box ausgeführt wird (nutzt handlerargs, erwartet also nur 1 Argument!)
|
|
47
|
+
// - handlerCancel: ist die Funktion die bei Ablehnung der Box ausgeführt wird (nutzt handlerargs, erwartet also nur 1 Argument!)
|
|
48
|
+
// - addCloseButton: boolscher Wert, der angibt, ob ein x oben rechts als close-Button verfügbar sein soll (bricht die Aktion ohne handler ab, standardmäßig false)
|
|
49
|
+
// - handlerArgs: kann im handler als weitere Argumente genutzt werden
|
|
50
|
+
// - activateConfirm: ist ein Boolean, der angibt ob die ConfirmationBox angezeigt werden soll oder nicht (wenn false, wird direkt handlerOk aufgerufen) (Standardmäßig true wenn nicht anders angegeben)
|
|
51
|
+
// - proceedButtonStyle: ist der Style des Bestätigungsbuttons (Standardmäßig unverändert)
|
|
52
|
+
// - cancelButtonStyle: ist der Style des Abbrechenbuttons (Standardmäßig unverändert)
|
|
53
|
+
|
|
54
|
+
export function ConfirmationBox({ state, setState }: { state: ConfirmationBoxState, setState: (state: ConfirmationBoxState) => void }): React.ReactElement {
|
|
55
|
+
|
|
56
|
+
// Wird verwendet um die Confirmation Boxen ein- und auszublenden
|
|
57
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
58
|
+
|
|
59
|
+
// Damit der Fokus auf die Confirmation Box gesetzt wird, wenn sie geöffnet wird
|
|
60
|
+
// Und man sie mit Enter bestätigen kann
|
|
61
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
62
|
+
|
|
63
|
+
// Sobald der State von außen aktualisiert wird triggert diese Funktion
|
|
64
|
+
// Die setzt showConfirm auf true (oder ruft die übergebene Funktion auf wenn confirmationBoxen disabled sind)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (state?.handlerOk) {
|
|
67
|
+
if (state.activateConfirm ?? true) {
|
|
68
|
+
setShowConfirm(true);
|
|
69
|
+
setTimeout(() => { document.getElementById("confirmButton")?.focus(); }, 1);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Wenn activateConfirm explizit false ist, dann wird die übergebene Funktion direkt ausgeführt ohne ConfirmationBox
|
|
73
|
+
if (typeof state.handlerOk === 'function')
|
|
74
|
+
state.handlerOk(state.handlerArgs);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
overlayRef.current?.focus();
|
|
79
|
+
}, 0);
|
|
80
|
+
}, [state]);
|
|
81
|
+
|
|
82
|
+
const handleAction = (handler: ((args?: unknown) => void) | undefined) : void => {
|
|
83
|
+
setShowConfirm(false);
|
|
84
|
+
const tempState = {
|
|
85
|
+
handler: handler,
|
|
86
|
+
handlerArgs: state.handlerArgs
|
|
87
|
+
};
|
|
88
|
+
setState(defaultConfirmationState);
|
|
89
|
+
if (typeof tempState.handler === 'function')
|
|
90
|
+
tempState.handler(tempState.handlerArgs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return showConfirm ?
|
|
94
|
+
<div className="confirmation-overlay"
|
|
95
|
+
tabIndex={0}
|
|
96
|
+
ref={overlayRef}>
|
|
97
|
+
<div className="confirmation-box">
|
|
98
|
+
{state.addCloseButton?
|
|
99
|
+
<span className="closeButton">
|
|
100
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
101
|
+
tabIndex={0}
|
|
102
|
+
className="close-icon"
|
|
103
|
+
onClick={() => handleAction(undefined)}
|
|
104
|
+
onKeyDown={(e) => {
|
|
105
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
106
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
107
|
+
handleAction(undefined);
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
role="button" aria-label="Dialog schließen"
|
|
111
|
+
width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
112
|
+
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
113
|
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
114
|
+
</svg>
|
|
115
|
+
</span> :
|
|
116
|
+
<></>}
|
|
117
|
+
{state?.headline !== undefined ? <p className="headline" style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}><strong>{state?.headline}</strong></p> : <></>}
|
|
118
|
+
<p style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}>{state.message1 ?? state.message ?? ""}</p>
|
|
119
|
+
<p style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}><strong>{state.message2 ?? ""}</strong></p>
|
|
120
|
+
<div className="confirmation-buttons">
|
|
121
|
+
<button
|
|
122
|
+
onClick={() => handleAction(state.handlerCancel)}
|
|
123
|
+
onKeyDown={(e) => {
|
|
124
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
125
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
126
|
+
handleAction(state.handlerCancel);
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
className="px-4 py-2 bg-gray-300 rounded"
|
|
130
|
+
style={state?.cancelButtonStyle !== undefined ? state.cancelButtonStyle : {}}>
|
|
131
|
+
{state.cancelButtonText ? state.cancelButtonText : "Abbrechen"}
|
|
132
|
+
</button>
|
|
133
|
+
<button onClick={() => handleAction(state.handlerOk)}
|
|
134
|
+
id="confirmButton"
|
|
135
|
+
onKeyDown={(e) => {
|
|
136
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
137
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
138
|
+
handleAction(state.handlerOk);
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
142
|
+
style={state?.proceedButtonStyle !== undefined ? state.proceedButtonStyle : {}}>
|
|
143
|
+
{state.proceedButtonText ? state.proceedButtonText : "OK"}
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div> : <></>;
|
|
148
|
+
}
|
package/Dropdown.css
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.custom-select {
|
|
2
|
+
border-radius: 8px;
|
|
3
|
+
border: 1px solid var(--Mittelgrau);
|
|
4
|
+
color: black;
|
|
5
|
+
background-color: white;
|
|
6
|
+
-moz-box-align: center;
|
|
7
|
+
align-items: center;
|
|
8
|
+
cursor: default;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
-moz-box-pack: justify;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
margin-bottom: 0.5rem;
|
|
14
|
+
font-size: 0.8em;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.dropdown__control {
|
|
18
|
+
width: 100%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.custom-select:hover, .dropdown__menu-list div:hover, .card .dropdown__menu-list div:hover {
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dropdown__multi-value__remove {
|
|
26
|
+
color: var(--FastSchwarz, black);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.custom-select .dropdown__single-value {
|
|
30
|
+
color: var(--FastSchwarz, black);
|
|
31
|
+
}
|
package/Dropdown.tsx
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useRef, useEffect, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import Select, { SingleValue, MultiValue} from 'react-select'
|
|
4
|
+
import type { CSSObject } from '@emotion/react';
|
|
5
|
+
import './Dropdown.css';
|
|
6
|
+
|
|
7
|
+
// Um das Dropdown Menü zu nutzen, müssen die Optionen das folgende Interface nutzen:
|
|
8
|
+
export interface Option<T = string> {
|
|
9
|
+
value: T;
|
|
10
|
+
label: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
indent?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Wobei alle Attribute außer dem value und dem label optional sind:
|
|
16
|
+
// - value: Der Wert, der bei Auswahl gesetzt werden soll
|
|
17
|
+
// -> Dabei kann es sich um komplexe Objekte handeln, diese müssen dann ein Attribut id haben, mithilfe derer sie verglichen werden!
|
|
18
|
+
// - label: Der String, der in der Auswahl angezeigt werden soll
|
|
19
|
+
// - disabled: Kann auf true gesetzt werden, wenn diese Option nicht zur Auswahl verfügbar sein soll
|
|
20
|
+
// - indent: Eine Zahl größer 0, kann angegeben werden, wenn die Option mit so vielen Strichen eingerückt werden soll (nur im Auswahlmenü)
|
|
21
|
+
|
|
22
|
+
// Attribute vom Dropdown:
|
|
23
|
+
// - selections: Das Array mit den Optionen die zur Auswahl stehen sollen
|
|
24
|
+
// - value: Der Wert, der ausgewählt angezeigt werden soll (useState)
|
|
25
|
+
// - onChange: Die Funktion die bei Änderung ausgeführt werden soll.
|
|
26
|
+
// Standardmäßig könnte z.B. {(value) => {value ? setValue(value) : setValue("")}} genutzt werden, wenn es sich bei value um einen String handelt
|
|
27
|
+
// - maxMenuHeight: Maximale Menühöhe in Pixeln (Optional)
|
|
28
|
+
// - menuPlacement: Ob das Menü oben oder unten platziert sein soll (Optional, Standard: Auto)
|
|
29
|
+
// - width: Breite des Menüs als CSS Property (Optional)
|
|
30
|
+
// - placeHolder: Der Platzhalter der angezeigt wird, wenn noch nichts ausgewählt ist (Optional) (Wird auch nur angezeigt, wenn value noch keinen validen Wert hat)
|
|
31
|
+
// - defaultValue: Die Auswahl die von Anfang an ausgewählt ist (Optional) (sollten mehrere Werte übergeben werden, wird "isMulti" automatisch gesetzt)
|
|
32
|
+
// - isMulti: Ob mehrere Optionen gleichzeitig ausgewählt werden können (Optional)
|
|
33
|
+
// - Daraufhin MUSS value ein Array sein und falls defaultValue angegeben ist, MUSS das auch ein Array sein
|
|
34
|
+
// - cardColorVariant: Wenn auf true gesetzt, wird eine leicht andere Farbvariante gewählt
|
|
35
|
+
// - ignoreDarkMode: Wenn auf true gesetzt wird der DarkMode ignoriert, der sonst standardmäßig angewendet wird
|
|
36
|
+
// - ...rest (Optional): sorgt dafür, dass alle weiteren angegebenen Attribute (z.B. aria-label) direkt an React-Select weitergegeben werden
|
|
37
|
+
|
|
38
|
+
export function Dropdown<T = string>({ selections, value, onChange, maxMenuHeight, menuPlacement, width, placeHolder, defaultValue, isMulti = false, cardColorVariant = false, ignoreDarkMode = false, ...rest} :
|
|
39
|
+
{
|
|
40
|
+
selections: Option<T>[],
|
|
41
|
+
value: T | T[],
|
|
42
|
+
onChange: (value: T | T[] | undefined) => void,
|
|
43
|
+
maxMenuHeight?: number,
|
|
44
|
+
menuPlacement?: "top" | "bottom",
|
|
45
|
+
width?: React.CSSProperties["width"],
|
|
46
|
+
placeHolder?: string,
|
|
47
|
+
defaultValue?: Option<T> | Option<T>[],
|
|
48
|
+
isMulti?: boolean,
|
|
49
|
+
cardColorVariant?: boolean,
|
|
50
|
+
ignoreDarkMode?: boolean,
|
|
51
|
+
}): React.ReactElement {
|
|
52
|
+
|
|
53
|
+
const initializedRef = useRef(false);
|
|
54
|
+
|
|
55
|
+
const options = selections.map(({value, label, disabled, indent}) => ({value: value, label: label, isDisabled: disabled ?? false, indent}));
|
|
56
|
+
|
|
57
|
+
if(!initializedRef.current && Array.isArray(value) && value.length === 0 && Array.isArray(defaultValue) && defaultValue.length > 0) {
|
|
58
|
+
value = options.filter(opt => (defaultValue as Option<T>[]).map(({value}) => (value)).includes(opt.value)).map(opt => opt.value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function usePrefersDarkMode() : boolean {
|
|
62
|
+
const [isDarkMode, setIsDarkMode] = useState(
|
|
63
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
68
|
+
const handler = (event: MediaQueryListEvent) : void => setIsDarkMode(event.matches);
|
|
69
|
+
|
|
70
|
+
// EventListener hinzufügen
|
|
71
|
+
mediaQuery.addEventListener('change', handler);
|
|
72
|
+
|
|
73
|
+
// Aufräumen
|
|
74
|
+
return () => mediaQuery.removeEventListener('change', handler);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return isDarkMode;
|
|
78
|
+
}
|
|
79
|
+
const isDarkModeRaw = usePrefersDarkMode()
|
|
80
|
+
const isDarkMode = ignoreDarkMode ? false : isDarkModeRaw;
|
|
81
|
+
|
|
82
|
+
function hasId(value: unknown): value is { id: any } {
|
|
83
|
+
return typeof value === "object" && value !== null && "id" in value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return isMulti || Array.isArray(defaultValue) ? (
|
|
87
|
+
<Select<Option<T>, true>
|
|
88
|
+
className = "custom-select"
|
|
89
|
+
isMulti={true}
|
|
90
|
+
placeholder = {placeHolder ?? ""}
|
|
91
|
+
options = {options}
|
|
92
|
+
value={options.length > 0 && hasId(options[0].value) ? // Wenn die values der Optionen ids haben, handelt es sich um komplexe Objekte, die zur Auswahl stehen und müssen dahingehend verglichen werden
|
|
93
|
+
options.filter(opt => (hasId(opt.value) && (value as T[]).find(val => hasId(val) && hasId(opt.value) && val.id === opt.value.id))) // Dann alle Optionen, die in den values per id vorkommen
|
|
94
|
+
: options.filter(opt => ((value as T[]).includes(opt.value))) // Ansonsten einfach alle Optionen, die in den values vorkommen
|
|
95
|
+
}
|
|
96
|
+
onChange={(selectedOption: MultiValue<Option<T>>) => {initializedRef.current = true; onChange(selectedOption.map((opt: Option<T>) => opt.value))}}
|
|
97
|
+
maxMenuHeight={maxMenuHeight ?? 280}
|
|
98
|
+
menuPlacement={menuPlacement ?? "auto"}
|
|
99
|
+
menuPosition="fixed"
|
|
100
|
+
classNamePrefix="dropdown"
|
|
101
|
+
defaultValue={options.filter(opt => (defaultValue as Option<T>[]).map(({value}) => (value)).includes(opt.value))}
|
|
102
|
+
formatOptionLabel={({ label, indent }: { label: string; indent?: number },{ context } : { context: string }) => {
|
|
103
|
+
if (context === 'menu') {
|
|
104
|
+
// Alle mit indent angegebenen Werte im Dropdown-Menü einrücken
|
|
105
|
+
return indent && indent > 0 ? '-'.repeat(indent) + '\u00A0' + label : label;
|
|
106
|
+
} else {
|
|
107
|
+
// Im ausgewählten Zustand: normal
|
|
108
|
+
return label;
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
styles={{
|
|
112
|
+
container: (provided: CSSObject) => ({
|
|
113
|
+
...provided,
|
|
114
|
+
backgroundColor: isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
115
|
+
color: isDarkMode? 'white' : 'black',
|
|
116
|
+
...(width ? { width } : {})
|
|
117
|
+
}),
|
|
118
|
+
control: () => ({
|
|
119
|
+
display: 'flex',
|
|
120
|
+
...(width ? { width } : {})
|
|
121
|
+
}),
|
|
122
|
+
menu: (provided: CSSObject) => ({
|
|
123
|
+
...provided,
|
|
124
|
+
borderRadius: '8px', // runde Ecken für das Dropdown
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
backgroundColor: isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
127
|
+
color: isDarkMode? 'white' : 'black',
|
|
128
|
+
...(width ? { width } : {})
|
|
129
|
+
}),
|
|
130
|
+
menuList: (provided: CSSObject) => ({
|
|
131
|
+
...provided,
|
|
132
|
+
overflowY: "auto",
|
|
133
|
+
padding: 0,
|
|
134
|
+
backgroundColor: isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
135
|
+
color: isDarkMode? 'white' : 'black',
|
|
136
|
+
}),
|
|
137
|
+
option: (provided: CSSObject, state: { isFocused: boolean; isSelected: boolean }) => ({
|
|
138
|
+
...provided,
|
|
139
|
+
backgroundColor: state.isFocused ? 'var(--DunkelAkzent, red)' : state.isSelected ? 'var(--MittelAkzent, blue)' :
|
|
140
|
+
isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
141
|
+
color: state.isFocused ? 'white' : state.isSelected ? 'var(--FastSchwarz, black)' :
|
|
142
|
+
isDarkMode? (cardColorVariant ? 'white' : 'white') : cardColorVariant ? 'black' : 'var(--FastSchwarz, black)',
|
|
143
|
+
cursor: 'pointer',
|
|
144
|
+
}),
|
|
145
|
+
}}
|
|
146
|
+
{...rest}
|
|
147
|
+
/>
|
|
148
|
+
) : (
|
|
149
|
+
<Select<Option<T>, false>
|
|
150
|
+
className = "custom-select"
|
|
151
|
+
isMulti={false}
|
|
152
|
+
placeholder = {placeHolder ?? ""}
|
|
153
|
+
options = {options}
|
|
154
|
+
value={options.length > 0 && hasId(options[0].value) ? // Wenn die values der Optionen ids haben, handelt es sich um komplexe Objekte, die zur Auswahl stehen und müssen dahingehend verglichen werden
|
|
155
|
+
options.find(opt => hasId(opt.value) && hasId(value) && opt.value.id === value.id) // Die eine Option mit der passenden id finden
|
|
156
|
+
: options.find(opt => opt.value === value)} // Ohne Multi oder id nur die eine Option finden
|
|
157
|
+
|
|
158
|
+
onChange={(selectedOption: SingleValue<Option<T>>) => onChange(selectedOption?.value ?? undefined)}
|
|
159
|
+
maxMenuHeight={maxMenuHeight ?? 280}
|
|
160
|
+
menuPlacement={menuPlacement ?? "auto"}
|
|
161
|
+
menuPosition="fixed"
|
|
162
|
+
classNamePrefix="dropdown"
|
|
163
|
+
defaultValue={defaultValue}
|
|
164
|
+
formatOptionLabel={({ label, indent }: { label: string; indent?: number },{ context } : { context: string }) => {
|
|
165
|
+
if (context === 'menu') {
|
|
166
|
+
// Alle mit indent angegebenen Werte im Dropdown-Menü einrücken
|
|
167
|
+
return indent && indent > 0 ? '-'.repeat(indent) + '\u00A0' + label : label;
|
|
168
|
+
} else {
|
|
169
|
+
// Im ausgewählten Zustand: normal
|
|
170
|
+
return label;
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
styles={{
|
|
174
|
+
container: (provided: CSSObject) => ({
|
|
175
|
+
...provided,
|
|
176
|
+
backgroundColor: isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
177
|
+
color: isDarkMode? 'white' : 'black',
|
|
178
|
+
...(width ? { width } : {})
|
|
179
|
+
}),
|
|
180
|
+
control: () => ({
|
|
181
|
+
display: 'flex',
|
|
182
|
+
...(width ? { width } : {})
|
|
183
|
+
}),
|
|
184
|
+
menu: (provided: CSSObject) => ({
|
|
185
|
+
...provided,
|
|
186
|
+
borderRadius: '8px', // runde Ecken für das Dropdown
|
|
187
|
+
overflow: 'hidden',
|
|
188
|
+
backgroundColor: isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
189
|
+
color: isDarkMode? 'white' : 'black',
|
|
190
|
+
...(width ? { width } : {})
|
|
191
|
+
}),
|
|
192
|
+
menuList: (provided: CSSObject) => ({
|
|
193
|
+
...provided,
|
|
194
|
+
overflowY: "auto",
|
|
195
|
+
padding: 0,
|
|
196
|
+
backgroundColor: isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
197
|
+
color: isDarkMode? 'white' : 'black',
|
|
198
|
+
}),
|
|
199
|
+
option: (provided: CSSObject, state: { isFocused: boolean; isSelected: boolean }) => ({
|
|
200
|
+
...provided,
|
|
201
|
+
backgroundColor: state.isFocused ? 'var(--DunkelAkzent, red)' : state.isSelected ? 'var(--MittelAkzent, blue)' :
|
|
202
|
+
isDarkMode? (cardColorVariant ? 'var(--FastSchwarz, black)' : 'black') : cardColorVariant ? 'var(--FastWeiß, white)' : 'white',
|
|
203
|
+
color: state.isFocused ? 'white' : state.isSelected ? 'var(--FastSchwarz, black)' :
|
|
204
|
+
isDarkMode? (cardColorVariant ? 'white' : 'white') : cardColorVariant ? 'black' : 'var(--FastSchwarz, black)',
|
|
205
|
+
cursor: 'pointer',
|
|
206
|
+
}),
|
|
207
|
+
}}
|
|
208
|
+
{...rest}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
}
|
package/InfoOverlay.css
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
.information-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
width: 100vw;
|
|
6
|
+
height: 100vh;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
z-index: 10000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.information-box {
|
|
15
|
+
background-color: var(--Hellgrau, white);
|
|
16
|
+
color: var(--HellSchwarz,#2c2c2c);
|
|
17
|
+
padding: 2rem;
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
|
|
20
|
+
max-width: 500px;
|
|
21
|
+
width: 90%;
|
|
22
|
+
text-align: center;
|
|
23
|
+
animation: fadeIn 0.3s ease-in-out;
|
|
24
|
+
white-space: pre-line;
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.information-box .headline {
|
|
29
|
+
margin-bottom: 0.75em;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@keyframes fadeIn {
|
|
33
|
+
from {
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transform: scale(0.95);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
to {
|
|
39
|
+
opacity: 1;
|
|
40
|
+
transform: scale(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.information-box .information-buttons {
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
gap: 1rem;
|
|
48
|
+
margin-top: 1.5rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.information-box .information-buttons button {
|
|
52
|
+
font-size: 85%;
|
|
53
|
+
padding: 0.5rem 1.1rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.information-box .closeButton {
|
|
57
|
+
right: 10px;
|
|
58
|
+
top: 10px;
|
|
59
|
+
position: absolute;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.information-box .close-icon:hover {
|
|
63
|
+
color: var(--HSDAkzent);
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
package/InfoOverlay.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useState, CSSProperties } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import './InfoOverlay.css';
|
|
4
|
+
|
|
5
|
+
// Um das OnfoOverlay zu nutzen, muss der State die folgende Struktur haben:
|
|
6
|
+
export interface InfoOverlayState {
|
|
7
|
+
headline?: string | undefined;
|
|
8
|
+
message?: string | undefined;
|
|
9
|
+
proceedButtonText?: string | undefined;
|
|
10
|
+
handler?: ((args?: unknown) => void) | undefined;
|
|
11
|
+
handlerArgs?: unknown;
|
|
12
|
+
addCloseButton?: boolean | undefined;
|
|
13
|
+
style?: CSSProperties | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Der standard InfoOverlay Status, der zur Initialisierung genutzt werden kann
|
|
17
|
+
export const defaultInfoOverlayState: InfoOverlayState = {
|
|
18
|
+
headline: undefined,
|
|
19
|
+
message: undefined,
|
|
20
|
+
proceedButtonText: undefined,
|
|
21
|
+
handler: undefined,
|
|
22
|
+
handlerArgs: undefined,
|
|
23
|
+
addCloseButton: false,
|
|
24
|
+
style: undefined,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Wobei alle Attribute grundsätzlich optional sind:
|
|
28
|
+
// - headline: ist die Überschrift und wird fett hinterlegt
|
|
29
|
+
// - message: ist die angezeigte Nachricht
|
|
30
|
+
// - proceedButtonText: ist der Text der auf dem Procceed Button stehen soll (ohne Angabe wird "OK" verwendet)
|
|
31
|
+
// - handler: Funktion, die optional beim Bestätigen ausgeführt werden kann (Struktur: handler(args))
|
|
32
|
+
// - handlerArgs: kann im handler als Argumente genutzt werden
|
|
33
|
+
// - addCloseButton: boolscher Wert, der angibt, ob ein x oben rechts als close-Button verfügbar sein soll (bricht die Aktion ohne handler ab, standardmäßig false)
|
|
34
|
+
// - style: ist der Style der Information-Box (Standardmäßig unverändert)
|
|
35
|
+
|
|
36
|
+
export function InfoOverlay({ state, setState }: { state: InfoOverlayState, setState: (state: InfoOverlayState) => void }): React.ReactElement {
|
|
37
|
+
|
|
38
|
+
// Wird verwendet um das Infoverlay ein- und auszublenden
|
|
39
|
+
const [showOverlay, setShowOverlay] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Sobald der State von außen aktualisiert wird triggert diese Funktion
|
|
42
|
+
// Die setzt showOverlay auf true
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (state?.message !== undefined || state?.headline !== undefined) {
|
|
45
|
+
setShowOverlay(true);
|
|
46
|
+
setTimeout(() => { document.getElementById("infoButton")?.focus(); }, 5);
|
|
47
|
+
}
|
|
48
|
+
}, [state]);
|
|
49
|
+
|
|
50
|
+
const handleAction = () : void => {
|
|
51
|
+
setShowOverlay(false);
|
|
52
|
+
const tempState = {
|
|
53
|
+
handler: state.handler,
|
|
54
|
+
handlerArgs: state.handlerArgs
|
|
55
|
+
};
|
|
56
|
+
setState(defaultInfoOverlayState);
|
|
57
|
+
if (typeof tempState.handler === 'function')
|
|
58
|
+
tempState.handler(tempState.handlerArgs);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return showOverlay ?
|
|
62
|
+
<div className="information-overlay"
|
|
63
|
+
tabIndex={0}>
|
|
64
|
+
<div className="information-box" style={state?.style !== undefined ? state.style : {}}>
|
|
65
|
+
{state.addCloseButton?
|
|
66
|
+
<span className="closeButton">
|
|
67
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
68
|
+
tabIndex={0}
|
|
69
|
+
className="close-icon"
|
|
70
|
+
onClick={() => setShowOverlay(false)}
|
|
71
|
+
onKeyDown={(e) => {
|
|
72
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
73
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
74
|
+
setShowOverlay(false);
|
|
75
|
+
}
|
|
76
|
+
}}
|
|
77
|
+
role="button" aria-label="Dialog schließen"
|
|
78
|
+
width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
79
|
+
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
80
|
+
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
81
|
+
</svg>
|
|
82
|
+
</span> :
|
|
83
|
+
<></>}
|
|
84
|
+
<p className="headline" tabIndex={0} style={{ whiteSpace: "pre-line" }}><strong>{state?.headline !== undefined ? state.headline : ""}</strong></p>
|
|
85
|
+
<p tabIndex={0} style={{ whiteSpace: "pre-line", wordBreak: "break-word" }}>{state?.message !== undefined ? state.message : ""}</p>
|
|
86
|
+
<div className="information-buttons">
|
|
87
|
+
<button onClick={() => handleAction()}
|
|
88
|
+
onKeyDown={(e) => {
|
|
89
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
90
|
+
e.preventDefault(); // Verhindert Scroll bei Space
|
|
91
|
+
handleAction();
|
|
92
|
+
}
|
|
93
|
+
}}
|
|
94
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
95
|
+
id="infoButton">
|
|
96
|
+
{state?.proceedButtonText !== undefined ? state.proceedButtonText : "OK"}
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div> : <></>;
|
|
101
|
+
}
|