Skip to content

Introduce KeyInterceptorService withs sync/async support to replace KeyInterceptor #9896

Merged
ScarletKuro merged 7 commits intoMudBlazor:devfrom
ScarletKuro:key_interceptor_new
Oct 4, 2024
Merged

Introduce KeyInterceptorService withs sync/async support to replace KeyInterceptor #9896
ScarletKuro merged 7 commits intoMudBlazor:devfrom
ScarletKuro:key_interceptor_new

Conversation

@ScarletKuro
Copy link
Member

@ScarletKuro ScarletKuro commented Oct 3, 2024

Description

Fixes: #8992
Fixes: #8583

NB! The old service is not removed, just slightly refactored. I don't think we should remove it, just obsolete and remove in next major release.

The API is quite flexible, that's how you can use it:

// The HandKeyDown / HandleKeyUp can be void or Task

// Only one handle (down)
await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, keyDown: HandleKeyDown);

// Only one handle (up)
await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, keyUp: HandleKeyUp);

// Two handlers at same time (down and up)
await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, keyDown: HandleKeyDown, keyUp: HandleKeyUp); 

// Alternative with KeyObserver utility. Everything internally is using the KeyObserver wrapper
// The reason for this to be exposed is that, unlike the above overloads, it allows you to make the first handler a Task and other void (and vice-versa).
// With the above ones, if you set one handler as Task, then all of them have to be Task, and the same applies to void.
await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, KeyObserver.KeyDown(HandleKeyDown), KeyObserver.KeyUpIgnore());

// Implementing IKeyInterceptorObserver directly on a component
await KeyInterceptorService.SubscribeAsync(this, keyInterceptorOptions);

View MudDialogInstance.razor.cs as example:

var keyInterceptorOptions = KeyInterceptorOptions.Create(
targetClass: "mud-dialog",
keys: KeyOptions.Of(key: "Escape", subscribeDown: true));
await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, keyDown: HandleKeyDown);

How Has This Been Tested?

Fully tested with unit tests

Type of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation (fix or improvement to the website or code docs)

Checklist

  • The PR is submitted to the correct branch (dev).
  • My code follows the code style of this project.
  • I've added relevant tests.

@github-actions github-actions bot added enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library PR: needs review labels Oct 3, 2024
@ScarletKuro ScarletKuro added hacktoberfest Hacktoberfest 2021 hacktoberfest-accepted Issues and PRs which were accepted as Hacktoberfest submissions labels Oct 3, 2024
@codecov
Copy link

codecov bot commented Oct 3, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 90.93%. Comparing base (28bc599) to head (cd9c403).
Report is 1103 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #9896      +/-   ##
==========================================
+ Coverage   89.82%   90.93%   +1.10%     
==========================================
  Files         412      411       -1     
  Lines       11878    12863     +985     
  Branches     2364     2486     +122     
==========================================
+ Hits        10670    11697    +1027     
+ Misses        681      618      -63     
- Partials      527      548      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ScarletKuro ScarletKuro added hacktoberfest-accepted Issues and PRs which were accepted as Hacktoberfest submissions and removed hacktoberfest-accepted Issues and PRs which were accepted as Hacktoberfest submissions labels Oct 3, 2024
Copy link
Contributor

@henon henon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome. I'll add the removal of KeyInterceptor to the v8 project.

@danielchalmers
Copy link
Member

  1. Should KeyOptions, KeyInterceptorOptions be immutable if they're not intended to be reused/modified as they require an explicit subscribe again? Allowing Keys to be IReadOnlyList<KeyOptions> and not need .ToList().

  2. I had success in building the solution after replacing the builders with new, and making key non-optional, turning this:

var keyInterceptorOptions = KeyInterceptorOptions.Create(
    targetClass: "mud-input-control",
    enableLogging: true,
    keys:
    [
        // prevent scrolling page, toggle open/close
        KeyOptions.Of(key: " ", preventDown: "key+none"),
        // prevent scrolling page, instead highlight previous item
        KeyOptions.Of(key: "ArrowUp", preventDown: "key+none"),
        // prevent scrolling page, instead highlight next item
        KeyOptions.Of(key: "ArrowDown", preventDown: "key+none"),
        KeyOptions.Of(key: "Home", preventDown: "key+none"),
        KeyOptions.Of(key: "End", preventDown: "key+none"),
        KeyOptions.Of(key: "Escape"),
        KeyOptions.Of(key: "Enter", preventDown: "key+none"),
        KeyOptions.Of(key: "NumpadEnter", preventDown: "key+none"),
        // select all items instead of all page text
        KeyOptions.Of(key: "a", preventDown: "key+ctrl"),
        // select all items instead of all page text
        KeyOptions.Of(key: "A", preventDown: "key+ctrl"),
        // for our users
        KeyOptions.Of(key: "/./", subscribeDown: true, subscribeUp: true)
    ]);

into:

var keyInterceptorOptions = new KeyInterceptorOptions(
    targetClass: "mud-input-control",
    enableLogging: true,
    // prevent scrolling page, toggle open/close
    new(" ", preventDown: "key+none"),
    // prevent scrolling page, instead highlight previous item
    new("ArrowUp", preventDown: "key+none"),
    // prevent scrolling page, instead highlight next item
    new("ArrowDown", preventDown: "key+none"),
    new("Home", preventDown: "key+none"),
    new("End", preventDown: "key+none"),
    new("Escape"),
    new("Enter", preventDown: "key+none"),
    new("NumpadEnter", preventDown: "key+none"),
    // select all items instead of all page text
    new("a", preventDown: "key+ctrl"),
    // select all items instead of all page text
    new("A", preventDown: "key+ctrl"),
    // for our users
    new("/./", subscribeDown: true, subscribeUp: true)
);

Is this feasible in reality?

@ScarletKuro
Copy link
Member Author

ScarletKuro commented Oct 3, 2024

  • Should KeyOptions, KeyInterceptorOptions be immutable if they're not intended to be reused/modified as they require an explicit subscribe again? Allowing Keys to be IReadOnlyList<KeyOptions> and not need .ToList().

I made KeyInterceptorOptions immutable as it's a new class, but we can't do the same with KeyOptions as it's an existing primitive and we need to have backward compatibility for now, as many 3rd party is using key interceptor.

@ScarletKuro
Copy link
Member Author

ScarletKuro commented Oct 3, 2024

I had success in building the solution after replacing the builders with new, and making key non-optional, turning this:

I don't think I understand this one.
Do you suggest to remove params from keys parameter? It's just that in this case it has to be the first argument, because you can't have non optional parameter after the optional parameters.
I also don't see how this syntax:

var keyInterceptorOptions = new KeyInterceptorOptions(
    targetClass: "mud-input-control",
    enableLogging: true,
    new(" ", preventDown: "key+none"),
    new("ArrowUp", preventDown: "key+none"),
);

without the [] brackets is possible? I also think actually less readable without the key name?

Or you just want to add two constructors to KeyOptions one default and other same as the KeyOptions.Of arguments if you dislike it?

@henon
Copy link
Contributor

henon commented Oct 3, 2024

  1. Should KeyOptions, KeyInterceptorOptions be immutable if they're not intended to be reused/modified as they require an explicit subscribe again? Allowing Keys to be IReadOnlyList<KeyOptions> and not need .ToList().

I remember a case where we needed to change the interceptor options at runtime. It was the PR #3393.

@ScarletKuro
Copy link
Member Author

  1. Should KeyOptions, KeyInterceptorOptions be immutable if they're not intended to be reused/modified as they require an explicit subscribe again? Allowing Keys to be IReadOnlyList<KeyOptions> and not need .ToList().

I remember a case where we needed to change the interceptor options at runtime. It was the PR #3393.

Currently it's not supported, not in the old and new implementation, I event explicitly mentioned that in remarks. If there is no urgent need for it I wouldn't implement it, it's possible to do it but keeping track of settings is not trivial code and you can see that in the browserviewportservice.

@henon
Copy link
Contributor

henon commented Oct 3, 2024

Hmm, strange. I thought the Close on ESC needed that update feature because it was added for it.

Edit: If you look at the #3393 description you see that it modifies the options because otherwise the ESC on an open Select would also close the dialog.

@ScarletKuro
Copy link
Member Author

ScarletKuro commented Oct 3, 2024

Hmm, strange. I thought the Close on ESC needed that update feature because it was added for it.

Edit: If you look at the #3393 description you see that it modifies the options because otherwise the ESC on an open Select would also close the dialog.

You are mixing the interceptor settings that were moved to separate KeyInterceptorOptions (logging, target class and initial key list) with KeyOptions. The mentioned PR is using UpdateKey which updates the KeyOptions on the fly. I thought you were talking about the interceptor settings and you want to change the target class on the fly (as example), which isn't supported.
The UpdateKey is on place:

Task UpdateKeyAsync(string elementId, KeyOptions option);

But the KeyOptions still can be immutable in this case since you just create a new instance with same key but different options. We just need to wait for a major version to do so when old interceptor is removed.

@danielchalmers
Copy link
Member

danielchalmers commented Oct 4, 2024

I had success in building the solution after replacing the builders with new, and making key non-optional, turning this:

I don't think I understand this one. Do you suggest to remove params from keys parameter? It's just that in this case it has to be the first argument, because you can't have non optional parameter after the optional parameters. I also don't see how this syntax:

var keyInterceptorOptions = new KeyInterceptorOptions(
    targetClass: "mud-input-control",
    enableLogging: true,
    new(" ", preventDown: "key+none"),
    new("ArrowUp", preventDown: "key+none"),
);

without the [] brackets is possible? I also think actually less readable without the key name?

Or you just want to add two constructors to KeyOptions one default and other same as the KeyOptions.Of arguments if you dislike it?

Right, I see what you mean about the optional parameters. Maybe making key, keys, targetClass required since they would always be specified while keeping a single constructor in each class for simplicity:

public KeyOptions(
    string? key,
    bool subscribeDown = false,
    bool subscribeUp = false,
    string preventDown = "none",
    string preventUp = "none",
    string stopDown = "none",
    string stopUp = "none")
{
    Key = key;
    PreventDown = preventDown;
    PreventUp = preventUp;
    SubscribeDown = subscribeDown;
    SubscribeUp = subscribeUp;
    StopDown = stopDown;
    StopUp = stopUp;
}
/// <summary>
/// Configuration options for key interception.
/// </summary>
public class KeyInterceptorOptions
{
    /// <summary>
    /// The CSS class of the target HTML element that should be observed for keyboard events.
    /// </summary>
    /// <remarks>
    /// Note: This must be a single class name.
    /// </remarks>
    public string TargetClass { get; }

    /// <summary>
    /// Specifies whether resize events should be logged in the browser's console.
    /// </summary>
    public bool EnableLogging { get; }

    /// <summary>
    /// A list of key options that define the keys to intercept and their respective configurations.
    /// </summary>
    public IReadOnlyList<KeyOptions> Keys { get; }

    /// <summary>
    /// Creates a new instance of <see cref="KeyInterceptorOptions"/> with the specified parameters.
    /// </summary>
    /// <param name="targetClass">The CSS class of the target HTML element.</param>
    /// <param name="keys">An array of <see cref="KeyOptions"/> defining the keys to intercept and their configurations.</param>
    /// <param name="enableLogging">Specifies whether to enable logging of resize events in the browser's console.</param>
    /// <returns>A new instance of <see cref="KeyInterceptorOptions"/>.</returns>
    public KeyInterceptorOptions(
        string targetClass,
        KeyOptions[] keys,
        bool enableLogging = false)
    {
        TargetClass = targetClass;
        Keys = keys;
        EnableLogging = enableLogging;
    }
}
-protected override async Task OnAfterRenderAsync(bool firstRender)
-{
-    if (firstRender)
-    {
-        var keyInterceptorOptions = KeyInterceptorOptions.Create(
-            targetClass: "mud-switch-base",
-            keys:
-            [
-                // prevent scrolling page, instead increment
-                KeyOptions.Of(key: "ArrowUp", preventDown: "key+none"),
-                // prevent scrolling page, instead decrement
-                KeyOptions.Of(key: "ArrowDown", preventDown: "key+none"),
-                KeyOptions.Of(key: " ", preventDown: "key+none", preventUp: "key+none")
-            ]);
-        await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, keyDown: HandleKeyDown);
-    }
-    await base.OnAfterRenderAsync(firstRender);
-}

+protected override async Task OnAfterRenderAsync(bool firstRender)
+{
+    if (firstRender)
+    {
+        var keyInterceptorOptions = new KeyInterceptorOptions(
+            "mud-switch-base",
+            [
+                // prevent scrolling page, instead increment
+                new("ArrowUp", preventDown: "key+none"),
+                // prevent scrolling page, instead decrement
+                new("ArrowDown", preventDown: "key+none"),
+                new(" ", preventDown: "key+none", preventUp: "key+none")
+            ]
+        );
+        await KeyInterceptorService.SubscribeAsync(_elementId, keyInterceptorOptions, keyDown: HandleKeyDown);
+    }
+    await base.OnAfterRenderAsync(firstRender);
+}

See previous edit for optional targetClass

@ScarletKuro
Copy link
Member Author

@danielchalmers done, tho I ended up adding several constructors to KeyInterceptorOptions.
It can actually allow you this syntax:

var keyInterceptorOptions = new KeyInterceptorOptions(
    targetClass: "mud-input-control",
    new(" ", preventDown: "key+none"),
    new("ArrowUp", preventDown: "key+none"),
);

and

var keyInterceptorOptions = new KeyInterceptorOptions(
    targetClass: "mud-input-control",
    enableLogging: true,
    new(" ", preventDown: "key+none"),
    new("ArrowUp", preventDown: "key+none"),
);

also [] can be used as discussed above
But as I said, I'm not really fan of such short entry so I will leave my ported code as is, but others can use it.

If there are no other suggestion then this can be merged.

Copy link
Member

@danielchalmers danielchalmers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are no other suggestion then this can be merged.

Not able to do a deep dive but it looks great! So glad to see async handlers along with a streamlined and opinionated API that can move accessibility implementations forward. Great work as always

@ScarletKuro ScarletKuro merged commit d37788f into MudBlazor:dev Oct 4, 2024
ingkor pushed a commit to ingkor/MudBlazor that referenced this pull request Oct 6, 2024
@ScarletKuro
Copy link
Member Author

Added to v8.0.0 Migration Guide #9953

@ScarletKuro
Copy link
Member Author

ScarletKuro commented Oct 11, 2024

Ah, I bamboozled myself.
I thought KeyInterceptorOptions was my own class, and not the class that existed before.
So when we made KeyInterceptorOptions immutable as @danielchalmers suggested #9896 (comment), we did break the CodeBeam.MudBlazor.Extension.
My fault, I forgot about it, since code logic was made 6 months ago.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Adds a new feature or enhances existing functionality (not fixing a defect) in the main library hacktoberfest Hacktoberfest 2021 hacktoberfest2024 hacktoberfest-accepted Issues and PRs which were accepted as Hacktoberfest submissions regression Previously worked and now doesn't

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

KeyInterceptor: Simplify setup

3 participants