Skip to content

Commit

Permalink
Merge branch 'feature/disable-hotkeys'
Browse files Browse the repository at this point in the history
  • Loading branch information
jsakamoto committed Apr 16, 2024
2 parents 7f72711 + 8c3e1aa commit 7273615
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 142 deletions.
83 changes: 83 additions & 0 deletions HotKeys2.E2ETest/HotKeysOnBrowserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,89 @@ public async Task ExcludeSelector_Test(HostingModel hostingModel)
await page.AssertEqualsAsync(_ => inputElement2.InputValueAsync(), "uu");
}

[Test]
[TestCaseSource(typeof(HotKeysOnBrowserTest), nameof(AllHostingModels))]
public async Task StateDisabled_Test(HostingModel hostingModel)
{
var context = TestContext.Instance;
var host = await context.StartHostAsync(hostingModel);

// Navigate to the "Counter" page,
var page = await context.GetPageAsync();
await page.GotoAndWaitForReadyAsync(host.GetUrl("/counter"));

// Verify the counter is 0.
var counter = page.Locator("h1+p");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 0");

// Set focus to the "Hotkeys are enabled in this field" input element, and type "U" key.
// Then the counter should not be incremented.
var inputElement1 = await page.QuerySelectorAsync(".disabled-state-hotkeys");
if (inputElement1 == null)
{
throw new InvalidOperationException("Test element is missing");
}

await inputElement1.FocusAsync();
await page.Keyboard.DownAsync("y");
await page.Keyboard.UpAsync("y");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 0");
await page.Keyboard.DownAsync("y");
await page.Keyboard.UpAsync("y");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 0");
await page.AssertEqualsAsync(_ => inputElement1.InputValueAsync(), "yy");
}

[Test]
[TestCaseSource(typeof(HotKeysOnBrowserTest), nameof(AllHostingModels))]
public async Task StateDisabledTrigger_Test(HostingModel hostingModel)
{
var context = TestContext.Instance;
var host = await context.StartHostAsync(hostingModel);

// Navigate to the "Counter" page,
var page = await context.GetPageAsync();
await page.GotoAndWaitForReadyAsync(host.GetUrl("/counter"));

// Verify the counter is 0.
var counter = page.Locator("h1+p");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 0");

// Set focus to the "Hotkeys are enabled in this field" input element, and type "U" key.
// Then the counter should not be incremented.
var inputElement1 = await page.QuerySelectorAsync(".disabled-state-hotkeys");
if (inputElement1 == null)
{
throw new InvalidOperationException("Test element is missing");
}

await inputElement1.FocusAsync();
await page.Keyboard.DownAsync("y");
await page.Keyboard.UpAsync("y");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 0");
await page.Keyboard.DownAsync("y");
await page.Keyboard.UpAsync("y");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 0");
await page.AssertEqualsAsync(_ => inputElement1.InputValueAsync(), "yy");

// Trigger disabled state
await page.ClickAsync(".state-trigger-button");

// Refocus and test again
// This time counter should increment
await inputElement1.FocusAsync();

await page.Keyboard.DownAsync("y");
await page.Keyboard.UpAsync("y");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 1");

await page.Keyboard.DownAsync("y");
await page.Keyboard.UpAsync("y");
await page.AssertEqualsAsync(_ => counter.TextContentAsync(), "Current count: 2");

await page.AssertEqualsAsync(_ => inputElement1.InputValueAsync(), "yy");
}

[Test]
[TestCaseSource(typeof(HotKeysOnBrowserTest), nameof(AllHostingModels))]
public async Task ByNativeKey_Test(HostingModel hostingModel)
Expand Down
2 changes: 1 addition & 1 deletion HotKeys2.E2ETest/Internals/SampleSite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async ValueTask<SampleSite> StartAsync()
// Publish and...
using var publishCommand = await Start(
"dotnet",
$"publish -f:{this.TargetFramework} -c:Release -p:BlazorEnableCompression=false -p:UsingBrowserRuntimeWorkload=false",
$"publish -f:{this.TargetFramework} -c:Release -p:BlazorEnableCompression=false -p:UsingBrowserRuntimeWorkload=false /p:BuildMode=test",
projDir)
.WaitForExitAsync();
publishCommand.ExitCode.Is(0, message: publishCommand.Output);
Expand Down
25 changes: 22 additions & 3 deletions HotKeys2/HotKeyEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public abstract class HotKeyEntry : IDisposable
/// </summary>
public string? Description { get; }

/// <summary>
/// Get the state data attached to this hot key entry.
/// </summary>
public HotKeyEntryState State { get; }

internal int Id = -1;

internal readonly DotNetObjectReference<HotKeyEntry> _ObjectRef;
Expand All @@ -55,14 +60,19 @@ public abstract class HotKeyEntry : IDisposable

private readonly ILogger? _Logger;

/// <summary>
/// Notifies when the property values of the state object has changed.
/// </summary>
internal Action<HotKeyEntry>? _NotifyStateChanged;

/// <summary>
/// Initialize a new instance of the HotKeyEntry class.
/// </summary>
/// <param name="logger">The instance of <see cref="ILogger"/> that is used to log the error message.</param>
/// <param name="mode">The mode that how to identificate the hot key.</param>
/// <param name="typeOfModifiers"></param>
/// <param name="modifiers">The combination of modifier flags</param>
/// <param name="keyEntry">The key or code of the hot key</param>
/// <param name="typeOfModifiers">The type of the modifier flags.</param>
/// <param name="modifiers">The combination of modifier flags.</param>
/// <param name="keyEntry">The key or code of the hot key.</param>
/// <param name="ownerOfAction">The instance of a Razor component that is an owner of the callback action method.</param>
/// <param name="options">The options for this hotkey entry.</param>
[DynamicDependency(nameof(InvokeAction), typeof(HotKeyEntry))]
Expand All @@ -77,6 +87,8 @@ internal HotKeyEntry(ILogger? logger, HotKeyMode mode, Type typeOfModifiers, int
this.Description = options.Description;
this.Exclude = options.Exclude;
this.ExcludeSelector = options.ExcludeSelector;
this.State = options.State;
this.State._NotifyStateChanged = () => this._NotifyStateChanged?.Invoke(this);
this._ObjectRef = DotNetObjectReference.Create(this);
}

Expand Down Expand Up @@ -108,12 +120,14 @@ protected void CommonProcess(Func<ValueTask> action)
/// <summary>
/// Returns a String that combined key combination and description of this entry, like "Ctrl+A: Select All."
/// </summary>
/// <returns>A string that represents the key combination and description of this entry.</returns>
public override string ToString() => this.ToString("{0}: {1}");

/// <summary>
/// Returns a String formatted with specified format string.
/// </summary>
/// <param name="format">{0} will be replaced with key combination text, and {1} will be replaced with description of this hotkey entry object.</param>
/// <returns>A string formatted with the specified format string.</returns>
public string ToString(string format)
{
var keyComboText = string.Join(" + ", this.ToStringKeys());
Expand All @@ -123,6 +137,7 @@ public string ToString(string format)
/// <summary>
/// Returns an array of String formatted keys.
/// </summary>
/// <returns>An array of string formatted keys.</returns>
public string[] ToStringKeys()
{
var keyCombo = new List<string>();
Expand All @@ -145,8 +160,12 @@ public string[] ToStringKeys()
return keyCombo.ToArray();
}

/// <summary>
/// Disposes the hot key entry.
/// </summary>
public void Dispose()
{
this.State._NotifyStateChanged = null;
this.Id = -1;
this._ObjectRef.Dispose();
}
Expand Down
23 changes: 23 additions & 0 deletions HotKeys2/HotKeyEntryState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Toolbelt.Blazor.HotKeys2;

/// <summary>
/// Represents the state of a hot key entry.
/// </summary>
public class HotKeyEntryState
{
/// <summary>
/// Notifies when the property values of this state object has changed.
/// </summary>
internal Action? _NotifyStateChanged;

private bool _Disabled;

/// <summary>
/// Controls if the current hot key is disabled or not.
/// </summary>
public virtual bool Disabled
{
get => this._Disabled;
set { if (this._Disabled != value) { this._Disabled = value; this._NotifyStateChanged?.Invoke(); } }
}
}
3 changes: 3 additions & 0 deletions HotKeys2/HotKeyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ public class HotKeyOptions

/// <summary>Additional CSS selector for HTML elements that will not allow hotkey to work.</summary>
public string ExcludeSelector { get; set; } = "";

/// <summary>State data attached to a hotkey.</summary>
public HotKeyEntryState State { get; set; } = new HotKeyEntryState();
}
33 changes: 22 additions & 11 deletions HotKeys2/HotKeysContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,7 @@ public HotKeysContext Add(ModKey modifiers, Key key, Func<HotKeyEntryByKey, Task
/// <param name="options">The options for this hotkey entry.</param>
/// <returns>This context.</returns>
private HotKeysContext AddInternal(ModKey modifiers, Key key, Func<HotKeyEntryByKey, ValueTask> action, IHandleEvent? ownerOfAction, HotKeyOptions options)
{
var hotkeyEntry = new HotKeyEntryByKey(this._Logger, modifiers, key, action, ownerOfAction, options);
lock (this.Keys) this.Keys.Add(hotkeyEntry);
var _ = this.RegisterAsync(hotkeyEntry);
return this;
}
=> this.AddInternal(new HotKeyEntryByKey(this._Logger, modifiers, key, action, ownerOfAction, options));

// ===============================================================================================

Expand Down Expand Up @@ -592,26 +587,40 @@ public HotKeysContext Add(ModCode modifiers, Code code, Func<HotKeyEntryByCode,
/// <param name="options">The options for this hotkey entry.</param>
/// <returns>This context.</returns>
private HotKeysContext AddInternal(ModCode modifiers, Code code, Func<HotKeyEntryByCode, ValueTask> action, IHandleEvent? ownerOfAction, HotKeyOptions options)
=> this.AddInternal(new HotKeyEntryByCode(this._Logger, modifiers, code, action, ownerOfAction, options));

// ===============================================================================================

private HotKeysContext AddInternal(HotKeyEntry hotkeyEntry)
{
var hotkeyEntry = new HotKeyEntryByCode(this._Logger, modifiers, code, action, ownerOfAction, options);
lock (this.Keys) this.Keys.Add(hotkeyEntry);
var _ = this.RegisterAsync(hotkeyEntry);
this.RegisterAsync(hotkeyEntry);
hotkeyEntry._NotifyStateChanged = this.OnNotifyStateChanged;
return this;
}

// ===============================================================================================


private async ValueTask RegisterAsync(HotKeyEntry hotKeyEntry)
private void RegisterAsync(HotKeyEntry hotKeyEntry)
{
await this.InvokeJsSafeAsync(async () =>
var _ = this.InvokeJsSafeAsync(async () =>
{
var module = await this._AttachTask;
if (this._IsDisposed) return;
hotKeyEntry.Id = await module.InvokeAsync<int>(
"Toolbelt.Blazor.HotKeys2.register",
hotKeyEntry._ObjectRef, hotKeyEntry.Mode, hotKeyEntry._Modifiers, hotKeyEntry._KeyEntry, hotKeyEntry.Exclude, hotKeyEntry.ExcludeSelector);
hotKeyEntry._ObjectRef, hotKeyEntry.Mode, hotKeyEntry._Modifiers, hotKeyEntry._KeyEntry, hotKeyEntry.Exclude, hotKeyEntry.ExcludeSelector, hotKeyEntry.State.Disabled);
});
}

private void OnNotifyStateChanged(HotKeyEntry hotKeyEntry)
{
var _ = this.InvokeJsSafeAsync(async () =>
{
var module = await this._AttachTask;
await module.InvokeVoidAsync("Toolbelt.Blazor.HotKeys2.update", hotKeyEntry.Id, hotKeyEntry.State.Disabled);
});
}

Expand Down Expand Up @@ -735,6 +744,7 @@ public HotKeysContext Remove(Func<IEnumerable<HotKeyEntry>, IEnumerable<HotKeyEn
{
var _ = this.UnregisterAsync(entry);
lock (this.Keys) this.Keys.Remove(entry);
entry._NotifyStateChanged = null;
}
return this;
}
Expand All @@ -748,6 +758,7 @@ public void Dispose()
foreach (var entry in this.Keys)
{
var _ = this.UnregisterAsync(entry);
entry._NotifyStateChanged = null;
}
this.Keys.Clear();
}
Expand Down
63 changes: 36 additions & 27 deletions HotKeys2/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,33 @@ export var Toolbelt;
var HotKeys2;
(function (HotKeys2) {
class HotkeyEntry {
constructor(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector) {
constructor(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled) {
this.dotNetObj = dotNetObj;
this.mode = mode;
this.modifiers = modifiers;
this.keyEntry = keyEntry;
this.exclude = exclude;
this.excludeSelector = excludeSelector;
this.isDisabled = isDisabled;
}
action() {
this.dotNetObj.invokeMethodAsync('InvokeAction');
}
}
let idSeq = 0;
const hotKeyEntries = new Map();
HotKeys2.register = (dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector) => {
HotKeys2.register = (dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled) => {
const id = idSeq++;
const hotKeyEntry = new HotkeyEntry(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector);
const hotKeyEntry = new HotkeyEntry(dotNetObj, mode, modifiers, keyEntry, exclude, excludeSelector, isDisabled);
hotKeyEntries.set(id, hotKeyEntry);
return id;
};
HotKeys2.update = (id, isDisabled) => {
const hotkeyEntry = hotKeyEntries.get(id);
if (!hotkeyEntry)
return;
hotkeyEntry.isDisabled = isDisabled;
};
HotKeys2.unregister = (id) => {
if (id === -1)
return;
Expand All @@ -38,7 +45,7 @@ export var Toolbelt;
return convertToKeyNameMap[ev.key] || ev.key;
};
const OnKeyDownMethodName = "OnKeyDown";
HotKeys2.attach = (hotKeysWrpper, isWasm) => {
HotKeys2.attach = (hotKeysWrapper, isWasm) => {
document.addEventListener('keydown', ev => {
if (typeof (ev["altKey"]) === 'undefined')
return;
Expand All @@ -52,37 +59,39 @@ export var Toolbelt;
const tagName = targetElement.tagName;
const type = targetElement.getAttribute('type');
const preventDefault1 = onKeyDown(modifiers, key, code, targetElement, tagName, type);
const preventDefault2 = isWasm === true ? hotKeysWrpper.invokeMethod(OnKeyDownMethodName, modifiers, tagName, type, key, code) : false;
const preventDefault2 = isWasm === true ? hotKeysWrapper.invokeMethod(OnKeyDownMethodName, modifiers, tagName, type, key, code) : false;
if (preventDefault1 || preventDefault2)
ev.preventDefault();
if (isWasm === false)
hotKeysWrpper.invokeMethodAsync(OnKeyDownMethodName, modifiers, tagName, type, key, code);
hotKeysWrapper.invokeMethodAsync(OnKeyDownMethodName, modifiers, tagName, type, key, code);
});
};
const onKeyDown = (modifiers, key, code, targetElement, tagName, type) => {
let preventDefault = false;
hotKeyEntries.forEach(entry => {
const byCode = entry.mode === 1;
const eventKeyEntry = byCode ? code : key;
const keyEntry = entry.keyEntry;
if (keyEntry !== eventKeyEntry)
return;
const eventModkeys = byCode ? modifiers : (modifiers & (0xffff ^ 1));
let entryModKeys = byCode ? entry.modifiers : (entry.modifiers & (0xffff ^ 1));
if (keyEntry.startsWith("Shift") && byCode)
entryModKeys |= 1;
if (keyEntry.startsWith("Control"))
entryModKeys |= 2;
if (keyEntry.startsWith("Alt"))
entryModKeys |= 4;
if (keyEntry.startsWith("Meta"))
entryModKeys |= 8;
if (eventModkeys !== entryModKeys)
return;
if (isExcludeTarget(entry, targetElement, tagName, type))
return;
preventDefault = true;
entry.action();
if (!entry.isDisabled) {
const byCode = entry.mode === 1;
const eventKeyEntry = byCode ? code : key;
const keyEntry = entry.keyEntry;
if (keyEntry !== eventKeyEntry)
return;
const eventModkeys = byCode ? modifiers : (modifiers & (0xffff ^ 1));
let entryModKeys = byCode ? entry.modifiers : (entry.modifiers & (0xffff ^ 1));
if (keyEntry.startsWith("Shift") && byCode)
entryModKeys |= 1;
if (keyEntry.startsWith("Control"))
entryModKeys |= 2;
if (keyEntry.startsWith("Alt"))
entryModKeys |= 4;
if (keyEntry.startsWith("Meta"))
entryModKeys |= 8;
if (eventModkeys !== entryModKeys)
return;
if (isExcludeTarget(entry, targetElement, tagName, type))
return;
preventDefault = true;
entry.action();
}
});
return preventDefault;
};
Expand Down
Loading

0 comments on commit 7273615

Please sign in to comment.