Skip to content

Commit 3342dcc

Browse files
authored
[dev-v5] FluentKeyCode (#3751)
1 parent 4cd877f commit 3342dcc

26 files changed

+1683
-49
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<FluentKeyCode OnKeyDown="@KeyDownHandler"
2+
PreventDefault="true"
3+
StopPropagation="true"
4+
Style="@CommonStyles.NeutralBorder1"
5+
Padding="@Padding.All3"
6+
tabindex="0">
7+
Click here and press <code>Ctrl</code> + <code>G</code>
8+
</FluentKeyCode>
9+
<div class="ma-4">
10+
@(shortcutPressed ? "Shortcut pressed!" : "")
11+
</div>
12+
13+
@code
14+
{
15+
bool shortcutPressed = false;
16+
17+
void KeyDownHandler(FluentKeyCodeEventArgs e)
18+
{
19+
if (e.Key == KeyCode.KeyG && e.CtrlKey)
20+
{
21+
shortcutPressed = true;
22+
}
23+
else
24+
{
25+
shortcutPressed = false;
26+
}
27+
}
28+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<FluentStack Wrap="true">
2+
<FluentKeyCode OnKeyDown="@KeyDownHandler">
3+
<div tabindex="0" style="width: 300px; height: 170px; @CommonStyles.NeutralBorder1 @CommonStyles.NeutralBorderShadow4" class="@Padding.All2">
4+
Click here and press <FluentKeyCode Id="my-any-key" tabindex="0" OnKeyDown="@KeyDownHandler" StopPropagation="true" Style="font-weight: bold;">any key</FluentKeyCode> to get the event keycode info.
5+
</div>
6+
</FluentKeyCode>
7+
8+
<ul>
9+
<li><span>Value:</span> <code>@LastKeyCode?.Value</code></li>
10+
<li><span>Key:</span> <code>@LastKeyCode?.Key.ToString()</code></li>
11+
<li><span>Code:</span> <code>@LastKeyCode?.KeyCode</code></li>
12+
<li>
13+
<span>Meta:</span>
14+
@if (LastKeyCode?.ShiftKey == true)
15+
{
16+
<FluentIcon Value="@(new Icons.Filled.Size20.KeyboardShift())" Color="Color.Primary" />
17+
}
18+
else
19+
{
20+
<FluentIcon Value="@(new Icons.Regular.Size20.KeyboardShift())" Color="Color.Default" />
21+
}
22+
@if (LastKeyCode?.CtrlKey == true)
23+
{
24+
<FluentIcon Value="@(new Icons.Filled.Size20.ControlButton())" Color="Color.Primary" />
25+
}
26+
else
27+
{
28+
<FluentIcon Value="@(new Icons.Regular.Size20.ControlButton())" Color="Color.Default" />
29+
}
30+
@if (LastKeyCode?.AltKey == true)
31+
{
32+
<FluentIcon Value="@(new Icons.Filled.Size20.KeyCommand())" Color="Color.Primary" />
33+
}
34+
else
35+
{
36+
<FluentIcon Value="@(new Icons.Regular.Size20.KeyCommand())" Color="Color.Default" />
37+
}
38+
@if (LastKeyCode?.MetaKey == true)
39+
{
40+
<FluentIcon Value="@(new Icons.Filled.Size20.ArrowBounce())" Color="Color.Primary" />
41+
}
42+
else
43+
{
44+
<FluentIcon Value="@(new Icons.Regular.Size20.ArrowBounce())" Color="Color.Default" />
45+
}
46+
</li>
47+
<li><span>Location:</span> <code>@LastKeyCode?.Location.ToString()</code></li>
48+
<li><span>TargetId:</span> <code>@LastKeyCode?.TargetId</code></li>
49+
<li><span>Repeat:</span> <code>@LastKeyCode?.Repeat</code></li>
50+
</ul>
51+
</FluentStack>
52+
53+
<style>
54+
li > span {
55+
float: left;
56+
width: 70px;
57+
font-weight: bold;
58+
}
59+
</style>
60+
61+
@code
62+
{
63+
FluentKeyCodeEventArgs? LastKeyCode;
64+
65+
void KeyDownHandler(FluentKeyCodeEventArgs e)
66+
{
67+
LastKeyCode = e;
68+
}
69+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
@inject IKeyCodeService KeyCodeService
2+
@implements IAsyncDisposable
3+
4+
<FluentStack Margin="@Margin.Bottom2" HorizontalGap="12px" Wrap="true">
5+
<FluentSwitch Label="Include KeyUp" @bind-Value="@IncludeKeyUp" />
6+
<FluentSwitch Label="Allow Repeat" @bind-Value="@StopRepeat"/>
7+
</FluentStack>
8+
9+
<FluentStack Style="@CommonStyles.NeutralBorder1" Wrap="true">
10+
@if (!KeyPressed.Any())
11+
{
12+
<div>Press any key to get the keycode info.</div>
13+
}
14+
15+
@foreach (var key in KeyPressed)
16+
{
17+
<span class="key" type="@key.Event">@key.Key</span>
18+
}
19+
</FluentStack>
20+
21+
@code
22+
{
23+
private bool IncludeKeyUp = false;
24+
private bool StopRepeat = false;
25+
private List<(string Key, string Event)> KeyPressed = new();
26+
27+
protected override void OnInitialized()
28+
{
29+
// FluentKeyCode.PreventMultipleKeyDown = true;
30+
KeyCodeService.RegisterListener(OnKeyHandleAsync, OnKeyHandleAsync);
31+
}
32+
33+
private Task OnKeyHandleAsync(FluentKeyCodeEventArgs args)
34+
{
35+
if (!IncludeKeyUp && args.Name == "keyup")
36+
{
37+
return Task.CompletedTask;
38+
}
39+
40+
if (StopRepeat && args.Repeat)
41+
{
42+
return Task.CompletedTask;
43+
}
44+
45+
KeyPressed.Add((args.ToString(), args.Name));
46+
StateHasChanged();
47+
return Task.CompletedTask;
48+
}
49+
50+
public ValueTask DisposeAsync()
51+
{
52+
KeyCodeService.UnregisterListener(OnKeyHandleAsync, OnKeyHandleAsync);
53+
return ValueTask.CompletedTask;
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* Basic keyboard key style */
2+
.key {
3+
display: inline-block;
4+
min-width: 30px;
5+
min-height: 30px;
6+
padding: 4px;
7+
margin: 4px;
8+
background-color: #f0f0f0;
9+
border: 1px solid #ccc;
10+
border-radius: 5px;
11+
text-align: center;
12+
font-weight: bold;
13+
font-size: 14px;
14+
color: black;
15+
cursor: pointer;
16+
}
17+
18+
.key[type="keyup"] {
19+
color: red;
20+
}
21+
22+
/* Hover effect */
23+
.key:hover {
24+
background-color: #e0e0e0;
25+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
26+
}
27+
28+
/* Active (pressed) effect */
29+
.key:active {
30+
background-color: #d0d0d0;
31+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
32+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
title: KeyCode
3+
route: /KeyCode
4+
---
5+
6+
# KeyCode
7+
8+
In some circumstances, Blazor does not retrieve all the **KeyDown** information received from JavaScript.
9+
`FluentKeyCode` retrieves this data, in a similar way to the [JavaScript KeyCode library](https://www.npmjs.com/package/keycode)
10+
and to [this demo sample](https://www.toptal.com/developers/keycode).
11+
12+
The `FluentKeyCode` component extends the functionality of **OnKeyDown** by adding the **KeyCode** parameter when the **OnKeyDown** event is raised.
13+
14+
## Example
15+
16+
{{ KeyCodeDefault }}
17+
18+
## Section
19+
20+
This example shows how to use the **FluentKeyCode** component to display all key details when a key is pressed.
21+
22+
{{ KeyCodeExample }}
23+
24+
## Global
25+
26+
You can also capture keystrokes **globally** on the current page.
27+
To do this, we provide you with a **IKeyCodeService** injected by the **AddFluentUIComponents** method.
28+
Add the following component at the end of your `MainLayout.razor` file.
29+
30+
```xml
31+
<FluentKeyCodeProvider />
32+
```
33+
34+
Once the provider and service have been injected, you can...
35+
36+
**Either**, retrieving the service and registering the method that will capture the keys:
37+
38+
```csharp
39+
[Inject]
40+
private IKeyCodeService KeyCodeService { get; set; }
41+
42+
protected override void OnInitialized()
43+
{
44+
KeyCodeService.RegisterListener(OnKeyDownAsync);
45+
}
46+
47+
public async Task OnKeyDownAsync(FluentKeyCodeEventArgs args) => { // ... }
48+
49+
public ValueTask DisposeAsync()
50+
{
51+
KeyCodeService.UnregisterListener(OnKeyDownAsync);
52+
return ValueTask.CompletedTask;
53+
}
54+
```
55+
56+
**Either**, implementing the interface **IKeyCodeListener**, retrieving the service and registering the method that will capture the keys:
57+
58+
```csharp
59+
public partial MyPage : IKeyCodeListener, IDisposableAsync
60+
{
61+
[Inject]
62+
private IKeyCodeService KeyCodeService { get; set; }
63+
64+
protected override void OnInitialized()
65+
{
66+
KeyCodeService.RegisterListener(this);
67+
}
68+
69+
public async Task OnKeyDownAsync(FluentKeyCodeEventArgs args) => { // ... }
70+
71+
public ValueTask DisposeAsync()
72+
{
73+
KeyCodeService.UnregisterListener(this);
74+
return ValueTask.CompletedTask;
75+
}
76+
}
77+
```
78+
79+
> Some keystrokes are used by the browser (e.g. `Ctrl + A`).
80+
> You can disable this function using the **PreventDefault** attribute with the **FluentKeyCodeProvider** component.
81+
> `<FluentKeyCodeProvider PreventDefault="true" />`
82+
83+
{{ KeyCodeGlobalExample }}
84+
85+
86+
## API FluentKeyCode
87+
88+
{{ API Type=FluentKeyCode }}
89+
90+
## API FluentKeyCodeEventArgs
91+
92+
{{ API Type=FluentKeyCodeEventArgs }}
93+
94+
## API FluentKeyCodeProvider
95+
96+
{{ API Type=FluentKeyCodeProvider }}
97+
98+
## Migrating to v5
99+
100+
No changes.

spelling.dic

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ datalist
1515
elementreference
1616
evenodd
1717
gzip
18+
henkan
1819
heure
1920
heures
2021
inputfile
2122
javascript
2223
jours
24+
keydown
25+
keyup
2326
maintenant
2427
menuchecked
2528
menuclicked
@@ -32,6 +35,7 @@ menuitemradioobsolete
3235
menuitems
3336
microsoft
3437
mois
38+
muhenkan
3539
myid
3640
noattribute
3741
nonfile

src/Core/Components/Base/FluentComponentBase.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,29 +77,41 @@ public abstract class FluentComponentBase : ComponentBase, IAsyncDisposable, IFl
7777
public virtual IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
7878

7979
/// <summary>
80-
/// Dispose the <see cref="JSModule"/> object.
80+
/// Dispose the current object.
8181
/// </summary>
8282
/// <returns></returns>
8383
/// <exception cref="NotImplementedException"></exception>
8484
[ExcludeFromCodeCoverage]
8585
public virtual async ValueTask DisposeAsync()
8686
{
87+
if (_jsModule != null)
88+
{
89+
try
90+
{
91+
await DisposeAsync(_jsModule.ObjectReference);
92+
}
93+
catch (Exception ex) when (ex is JSDisconnectedException ||
94+
ex is OperationCanceledException)
95+
{
96+
// The JSRuntime side may routinely be gone already if the reason we're disposing is that
97+
// the client disconnected. This is not an error.
98+
}
99+
}
100+
87101
_cachedServices?.DisposeTooltipAsync(this);
88102
_cachedServices?.Dispose();
89103
await JSModule.DisposeAsync();
90104
}
91105

92106
/// <summary>
93-
/// Dispose the <paramref name="jsModule"/> object.
107+
/// Override this method to call your custom dispose logic, using the <see cref="IJSObjectReference"/> object.
94108
/// </summary>
95109
/// <param name="jsModule"></param>
96110
/// <returns></returns>
97111
[ExcludeFromCodeCoverage]
98-
protected virtual async ValueTask DisposeAsync(IJSObjectReference? jsModule)
112+
protected virtual ValueTask DisposeAsync(IJSObjectReference jsModule)
99113
{
100-
_cachedServices?.DisposeTooltipAsync(this);
101-
_cachedServices?.Dispose();
102-
await JSModule.DisposeAsync(jsModule);
114+
return ValueTask.CompletedTask;
103115
}
104116

105117
/// <summary>

src/Core/Components/Base/FluentInputBase.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,22 +222,34 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
222222
[ExcludeFromCodeCoverage]
223223
public virtual async ValueTask DisposeAsync()
224224
{
225+
if (_jsModule != null)
226+
{
227+
try
228+
{
229+
await DisposeAsync(_jsModule.ObjectReference);
230+
}
231+
catch (Exception ex) when (ex is JSDisconnectedException ||
232+
ex is OperationCanceledException)
233+
{
234+
// The JSRuntime side may routinely be gone already if the reason we're disposing is that
235+
// the client disconnected. This is not an error.
236+
}
237+
}
238+
225239
_cachedServices?.DisposeTooltipAsync(this);
226240
_cachedServices?.Dispose();
227241
await JSModule.DisposeAsync();
228242
}
229243

230244
/// <summary>
231-
/// Dispose the <paramref name="jsModule"/> object.
245+
/// Override this method to call your custom dispose logic, using the <see cref="IJSObjectReference"/> object.
232246
/// </summary>
233247
/// <param name="jsModule"></param>
234248
/// <returns></returns>
235249
[ExcludeFromCodeCoverage]
236-
protected virtual async ValueTask DisposeAsync(IJSObjectReference? jsModule)
250+
protected virtual ValueTask DisposeAsync(IJSObjectReference jsModule)
237251
{
238-
_cachedServices?.DisposeTooltipAsync(this);
239-
_cachedServices?.Dispose();
240-
await JSModule.DisposeAsync(jsModule);
252+
return ValueTask.CompletedTask;
241253
}
242254

243255
/// <summary>

0 commit comments

Comments
 (0)