Idle detection

Be notified when the user goes idle (no input for a threshold) or the screen locks, via IIdleDetector (the Idle Detection API) — e.g. to auto-lock a session, pause a sync, or update presence in a collaborative app. WASM-only: the idle-detection permission needs a live gesture and the detector needs the live document.

RequestPermissionAsync() must run from a gesture; WatchAsync(onChange, thresholdSeconds) then pushes an IdleReading on each user/screen state change. The spec enforces a 60-second minimum threshold. Dispose the handle to stop.

IdleDetectorDemo.cs

using Rask.Site;
using Rask.Wasm.Browser;

namespace Rask.Site.Features;

/// <summary>
///     <see cref="IIdleDetector" /> — request the <c>idle-detection</c> permission from a gesture, then
///     watch for the user going idle or the screen locking. WASM-only: permission needs a live gesture and
///     the detector needs the live document.
/// </summary>
public sealed partial class IdleDetectorDemo(IIdleDetector idle) : Component, IAsyncDisposable
{
    private IAsyncDisposable? _watch;
    private string _user = "active";
    private string _screen = "unlocked";
    private string _status = "(idle)";

    protected override Component? Render() =>
        Div.Class($"{Tw.Card} shadow-sm border-0")[
            Div.Class(Tw.CardBody)[
                Button.Class($"{Tw.BtnPrimary} mb-3").Id("idle-start").OnClickAsync(Start)[
                    "Start watching (60s threshold)"],
                Div.Class("text-sm text-ui-muted")["User: ", Code.Id("idle-user")[_user]],
                Div.Class("text-sm text-ui-muted")["Screen: ", Code.Id("idle-screen")[_screen]],
                Div.Class("text-sm text-ui-muted")["Status: ", Code.Id("idle-status")[_status]]
            ]
        ];

    private async Task Start()
    {
        if (_watch is not null)
        {
            return;
        }

        try
        {
            if (!await idle.IsSupportedAsync())
            {
                _status = "Idle Detection not supported in this browser";
                return;
            }

            if (await idle.RequestPermissionAsync() != "granted")
            {
                _status = "Permission denied";
                return;
            }

            _watch = await idle.WatchAsync(reading =>
            {
                _user = reading.UserIdle ? "idle" : "active";
                _screen = reading.ScreenLocked ? "locked" : "unlocked";
                StateHasChanged();
                return Task.CompletedTask;
            });
            _status = "Watching — stop interacting for 60s to go idle";
        }
        catch (Exception ex)
        {
            _status = "Failed: " + ex.Message;
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (_watch is not null)
        {
            await _watch.DisposeAsync();
        }
    }
}
Live result
User: active
Screen: unlocked
Status: (idle)