Skip to content

Commit c432451

Browse files
committed
Add ReconnectModal for server reconnection handling
Introduce a new ReconnectModal component in the Blazor app to manage reconnection attempts when the server connection is lost. - Added <ReconnectModal /> to App.razor for displaying a modal dialog. - Implemented HTML structure in ReconnectModal.razor with messages and buttons for retrying or resuming connections. - Included a script tag to load a JavaScript module specific to the component. - Styled the modal with CSS for visual appeal, responsiveness, and animations. - Developed JavaScript logic in ReconnectModal.razor.js to handle reconnection states and user interactions. - Enhanced CSS for responsive design, dark theme support, and accessibility improvements.
1 parent d8750db commit c432451

3 files changed

Lines changed: 351 additions & 0 deletions

File tree

src/Server.UI/Components/App.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<body>
2727
<Routes @rendermode="RenderModeForPage"/>
28+
<ReconnectModal />
2829
<script src="_framework/blazor.web.js"></script>
2930
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
3031

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
<script type="module" src="@Assets["Components/ReconnectModal.razor.js"]"></script>
2+
<dialog id="components-reconnect-modal" data-nosnippet>
3+
<div class="components-reconnect-container">
4+
<div class="components-rejoining-animation" aria-hidden="true">
5+
<div></div>
6+
<div></div>
7+
</div>
8+
<p class="components-reconnect-first-attempt-visible">
9+
Rejoining the server...
10+
</p>
11+
<p class="components-reconnect-repeated-attempt-visible">
12+
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
13+
</p>
14+
<p class="components-reconnect-failed-visible">
15+
Failed to rejoin.<br />Please retry or reload the page.
16+
</p>
17+
<button id="components-reconnect-button" class="components-reconnect-failed-visible mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-button-filled-size-medium mud-ripple">
18+
<span class="mud-button-label">Retry</span>
19+
</button>
20+
<p class="components-pause-visible">
21+
The session has been paused by the server.
22+
</p>
23+
<button id="components-resume-button" class="components-pause-visible mud-button-root mud-button mud-button-filled mud-button-filled-primary mud-button-filled-size-medium mud-ripple">
24+
<span class="mud-button-label">Resume</span>
25+
</button>
26+
<p class="components-resume-failed-visible">
27+
Failed to resume the session.<br />Please reload the page.
28+
</p>
29+
</div>
30+
</dialog>
31+
<style>
32+
.components-reconnect-first-attempt-visible,
33+
.components-reconnect-repeated-attempt-visible,
34+
.components-reconnect-failed-visible,
35+
.components-pause-visible,
36+
.components-resume-failed-visible,
37+
.components-rejoining-animation {
38+
display: none;
39+
}
40+
41+
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
42+
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
43+
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
44+
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
45+
#components-reconnect-modal.components-reconnect-retrying,
46+
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
47+
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
48+
#components-reconnect-modal.components-reconnect-failed,
49+
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
50+
display: block;
51+
}
52+
53+
#components-reconnect-modal {
54+
background-color: var(--mud-palette-surface);
55+
color: var(--mud-palette-text-primary);
56+
width: 20rem;
57+
margin: 10vh auto;
58+
padding: 2rem;
59+
border: 0;
60+
border-radius: var(--mud-default-borderradius, 4px);
61+
box-shadow: var(--mud-elevation-8);
62+
opacity: 0;
63+
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
64+
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
65+
}
66+
67+
#components-reconnect-modal[open] {
68+
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
69+
animation-fill-mode: both;
70+
}
71+
72+
#components-reconnect-modal::backdrop {
73+
background-color: var(--mud-palette-backdrop-background);
74+
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
75+
opacity: 1;
76+
}
77+
78+
@@keyframes components-reconnect-modal-slideUp {
79+
0% {
80+
transform: translateY(30px) scale(0.95);
81+
}
82+
83+
100% {
84+
transform: translateY(0);
85+
}
86+
}
87+
88+
@@keyframes components-reconnect-modal-fadeInOpacity {
89+
0% {
90+
opacity: 0;
91+
}
92+
93+
100% {
94+
opacity: 1;
95+
}
96+
}
97+
98+
@@keyframes components-reconnect-modal-fadeOutOpacity {
99+
0% {
100+
opacity: 1;
101+
}
102+
103+
100% {
104+
opacity: 0;
105+
}
106+
}
107+
108+
.components-reconnect-container {
109+
display: flex;
110+
flex-direction: column;
111+
align-items: center;
112+
justify-content: flex-start;
113+
gap: 0.8rem;
114+
padding-top: 0.5rem;
115+
}
116+
#components-reconnect-modal::backdrop{
117+
backdrop-filter: blur(2px);
118+
}
119+
#components-reconnect-button,
120+
#components-resume-button {
121+
margin-top: 0.3rem; /* 自定义按钮间距 */
122+
}
123+
#components-reconnect-modal p {
124+
margin: 0;
125+
text-align: center;
126+
color: var(--mud-palette-text-primary);
127+
font-size: var(--mud-typography-default-size);
128+
line-height: 1.5;
129+
}
130+
131+
/* MudButton-like styling for reconnect buttons */
132+
#components-reconnect-modal button.mud-button-root {
133+
position: relative;
134+
display: inline-flex;
135+
align-items: center;
136+
justify-content: center;
137+
box-sizing: border-box;
138+
font-family: inherit;
139+
font-weight: 500;
140+
font-size: var(--mud-typography-default-size);
141+
line-height: 1.75;
142+
letter-spacing: 0.02857em;
143+
text-transform: uppercase;
144+
min-width: 64px;
145+
padding: 6px 16px;
146+
border-radius: var(--mud-default-borderradius, 4px);
147+
transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
148+
box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
149+
border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
150+
color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
151+
border: 0;
152+
cursor: pointer;
153+
outline: 0;
154+
text-decoration: none;
155+
vertical-align: middle;
156+
-webkit-user-select: none;
157+
-moz-user-select: none;
158+
user-select: none;
159+
-webkit-appearance: none;
160+
-webkit-tap-highlight-color: transparent;
161+
}
162+
#components-reconnect-modal button{
163+
margin: 0px auto !important;
164+
}
165+
#components-reconnect-modal button.mud-button-filled-primary {
166+
color: var(--mud-palette-primary-text);
167+
background-color: var(--mud-palette-primary);
168+
box-shadow: var(--mud-elevation-1);
169+
}
170+
171+
#components-reconnect-modal button.mud-button-filled-primary:hover {
172+
background-color: var(--mud-palette-primary-darken);
173+
box-shadow: var(--mud-elevation-4);
174+
}
175+
176+
#components-reconnect-modal button.mud-button-filled-primary:active {
177+
box-shadow: var(--mud-elevation-8);
178+
}
179+
180+
#components-reconnect-modal button.mud-button-root:disabled {
181+
color: var(--mud-palette-action-disabled);
182+
background-color: var(--mud-palette-action-disabled-background);
183+
box-shadow: none;
184+
cursor: default;
185+
pointer-events: none;
186+
}
187+
188+
#components-reconnect-modal button .mud-button-label {
189+
display: inherit;
190+
align-items: inherit;
191+
justify-content: inherit;
192+
}
193+
194+
/* Ripple effect container */
195+
#components-reconnect-modal button.mud-ripple {
196+
overflow: hidden;
197+
position: relative;
198+
}
199+
200+
#components-reconnect-modal button.mud-button-root:focus-visible {
201+
outline: 2px solid var(--mud-palette-primary);
202+
outline-offset: 2px;
203+
}
204+
205+
.components-rejoining-animation {
206+
position: relative;
207+
width: 80px;
208+
height: 80px;
209+
}
210+
211+
.components-rejoining-animation div {
212+
position: absolute;
213+
border: 3px solid var(--mud-palette-primary);
214+
opacity: 1;
215+
border-radius: 50%;
216+
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
217+
}
218+
219+
.components-rejoining-animation div:nth-child(2) {
220+
animation-delay: -0.5s;
221+
}
222+
223+
@@keyframes components-rejoining-animation {
224+
0% {
225+
top: 40px;
226+
left: 40px;
227+
width: 0;
228+
height: 0;
229+
opacity: 0;
230+
}
231+
232+
4.9% {
233+
top: 40px;
234+
left: 40px;
235+
width: 0;
236+
height: 0;
237+
opacity: 0;
238+
}
239+
240+
5% {
241+
top: 40px;
242+
left: 40px;
243+
width: 0;
244+
height: 0;
245+
opacity: 1;
246+
}
247+
248+
100% {
249+
top: 0px;
250+
left: 0px;
251+
width: 80px;
252+
height: 80px;
253+
opacity: 0;
254+
}
255+
}
256+
257+
/* Responsive design */
258+
@@media (max-width: 600px) {
259+
#components-reconnect-modal {
260+
width: 90%;
261+
max-width: 20rem;
262+
margin: 10vh auto;
263+
padding: 1.5rem;
264+
}
265+
}
266+
267+
/* Dark theme support */
268+
@@media (prefers-color-scheme: dark) {
269+
#components-reconnect-modal {
270+
background-color: var(--mud-palette-surface);
271+
color: var(--mud-palette-text-primary);
272+
}
273+
}
274+
275+
/* Accessibility improvements */
276+
@@media (prefers-reduced-motion: reduce) {
277+
#components-reconnect-modal,
278+
#components-reconnect-modal button,
279+
.components-rejoining-animation div {
280+
animation: none;
281+
transition: none;
282+
}
283+
}
284+
</style>
285+
@code {
286+
287+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Set up event handlers
2+
const reconnectModal = document.getElementById("components-reconnect-modal");
3+
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
4+
5+
const retryButton = document.getElementById("components-reconnect-button");
6+
retryButton.addEventListener("click", retry);
7+
8+
const resumeButton = document.getElementById("components-resume-button");
9+
resumeButton.addEventListener("click", resume);
10+
11+
function handleReconnectStateChanged(event) {
12+
if (event.detail.state === "show") {
13+
reconnectModal.showModal();
14+
} else if (event.detail.state === "hide") {
15+
reconnectModal.close();
16+
} else if (event.detail.state === "failed") {
17+
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
18+
} else if (event.detail.state === "rejected") {
19+
location.reload();
20+
}
21+
}
22+
23+
async function retry() {
24+
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
25+
26+
try {
27+
// Reconnect will asynchronously return:
28+
// - true to mean success
29+
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
30+
// - exception to mean we didn't reach the server (this can be sync or async)
31+
const successful = await Blazor.reconnect();
32+
if (!successful) {
33+
// We have been able to reach the server, but the circuit is no longer available.
34+
// We'll reload the page so the user can continue using the app as quickly as possible.
35+
const resumeSuccessful = await Blazor.resumeCircuit();
36+
if (!resumeSuccessful) {
37+
location.reload();
38+
} else {
39+
reconnectModal.close();
40+
}
41+
}
42+
} catch (err) {
43+
// We got an exception, server is currently unavailable
44+
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
45+
}
46+
}
47+
48+
async function resume() {
49+
try {
50+
const successful = await Blazor.resumeCircuit();
51+
if (!successful) {
52+
location.reload();
53+
}
54+
} catch {
55+
location.reload();
56+
}
57+
}
58+
59+
async function retryWhenDocumentBecomesVisible() {
60+
if (document.visibilityState === "visible") {
61+
await retry();
62+
}
63+
}

0 commit comments

Comments
 (0)