-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): added handleWakeLock utility
- Loading branch information
Showing
8 changed files
with
359 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
packages/core/src/browser/handle-wake-lock/index.svelte.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { describe, expect, it, vi } from 'vitest'; | ||
import { tick } from 'svelte'; | ||
import { handleWakeLock } from './index.svelte.js'; | ||
import { asyncEffectRoot } from '../../__internal__/utils.svelte.js'; | ||
|
||
class MockWakeLockSentinel extends EventTarget { | ||
released = false; | ||
|
||
release() { | ||
this.released = true; | ||
return Promise.resolve(); | ||
} | ||
} | ||
|
||
function defineWakeLockAPI() { | ||
const sentinel = new MockWakeLockSentinel(); | ||
|
||
Object.defineProperty(navigator, 'wakeLock', { | ||
value: { request: async () => sentinel as WakeLockSentinel }, | ||
writable: true | ||
}); | ||
|
||
return sentinel; | ||
} | ||
|
||
class MockDocument extends EventTarget { | ||
visibilityState = 'hidden'; | ||
} | ||
|
||
describe('Wake Lock API is not supported', () => { | ||
it("doesn't change isActive if it isn't supported", async () => { | ||
const cleanup = asyncEffectRoot(async () => { | ||
const wakeLock = handleWakeLock({ autoCleanup: false, navigator: {} as Navigator }); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
await wakeLock.request('screen'); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
await wakeLock.release(); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
wakeLock.cleanup(); | ||
}); | ||
|
||
await cleanup(); | ||
}); | ||
}); | ||
|
||
describe('Wake Lock API is supported', () => { | ||
it('changes isActive if it is supported', async () => { | ||
const cleanup = asyncEffectRoot(async () => { | ||
defineWakeLockAPI(); | ||
|
||
const wakeLock = handleWakeLock({ autoCleanup: false }); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
await wakeLock.forceRequest('screen'); | ||
|
||
expect(wakeLock.isActive).toBeTruthy(); | ||
|
||
await wakeLock.release(); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
wakeLock.cleanup(); | ||
}); | ||
|
||
await cleanup(); | ||
}); | ||
|
||
it('changes isActive if show other tabs or minimize window', async () => { | ||
const cleanup = asyncEffectRoot(async () => { | ||
vi.useFakeTimers(); | ||
defineWakeLockAPI(); | ||
|
||
const wakeLock = handleWakeLock({ autoCleanup: false }); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
await wakeLock.request('screen'); | ||
await vi.advanceTimersByTimeAsync(10); | ||
|
||
expect(wakeLock.isActive).toBeTruthy(); | ||
|
||
document.dispatchEvent(new window.Event('visibilitychange')); | ||
|
||
expect(wakeLock.isActive).toBeTruthy(); | ||
|
||
wakeLock.cleanup(); | ||
}); | ||
|
||
await cleanup(); | ||
}); | ||
|
||
it('delays requesting if the document is hidden', async () => { | ||
const cleanup = asyncEffectRoot(async () => { | ||
defineWakeLockAPI(); | ||
const mockDocument = new MockDocument(); | ||
|
||
const wakeLock = handleWakeLock({ autoCleanup: false, document: mockDocument as Document }); | ||
|
||
await wakeLock.request('screen'); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
mockDocument.visibilityState = 'visible'; | ||
mockDocument.dispatchEvent(new Event('visibilitychange')); | ||
|
||
await tick(); | ||
await tick(); | ||
|
||
expect(wakeLock.isActive).toBeTruthy(); | ||
|
||
wakeLock.cleanup(); | ||
}); | ||
|
||
await cleanup(); | ||
}); | ||
|
||
it('cancels requesting if release is called before the document becomes visible', async () => { | ||
const cleanup = asyncEffectRoot(async () => { | ||
defineWakeLockAPI(); | ||
const mockDocument = new MockDocument(); | ||
|
||
const wakeLock = handleWakeLock({ autoCleanup: false, document: mockDocument as Document }); | ||
|
||
await wakeLock.request('screen'); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
await wakeLock.release(); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
mockDocument.visibilityState = 'visible'; | ||
mockDocument.dispatchEvent(new Event('visibilitychange')); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
wakeLock.cleanup(); | ||
}); | ||
|
||
await cleanup(); | ||
}); | ||
|
||
it('becomes inactive if wake lock is released', async () => { | ||
const cleanup = asyncEffectRoot(async () => { | ||
const sentinel = defineWakeLockAPI(); | ||
const mockDocument = new MockDocument(); | ||
mockDocument.visibilityState = 'visible'; | ||
|
||
const wakeLock = handleWakeLock({ autoCleanup: false, document: mockDocument as Document }); | ||
|
||
await wakeLock.request('screen'); | ||
|
||
expect(wakeLock.isActive).toBeTruthy(); | ||
|
||
mockDocument.visibilityState = 'hidden'; | ||
mockDocument.dispatchEvent(new Event('visibilitychange')); | ||
sentinel.dispatchEvent(new Event('release')); | ||
|
||
expect(wakeLock.isActive).toBeFalsy(); | ||
|
||
mockDocument.visibilityState = 'visible'; | ||
mockDocument.dispatchEvent(new Event('visibilitychange')); | ||
await wakeLock.request('screen'); | ||
|
||
expect(wakeLock.isActive).toBeTruthy(); | ||
|
||
wakeLock.cleanup(); | ||
}); | ||
|
||
await cleanup(); | ||
}); | ||
}); |
116 changes: 116 additions & 0 deletions
116
packages/core/src/browser/handle-wake-lock/index.svelte.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { onDestroy } from 'svelte'; | ||
import { getDocumentVisibility } from '../../elements/index.js'; | ||
import { whenever } from '../../lifecycle/whenever/index.svelte.js'; | ||
import { handleEventListener } from '../handle-event-listener/index.svelte.js'; | ||
import { noop } from '../../__internal__/utils.svelte.js'; | ||
import { | ||
defaultDocument, | ||
defaultNavigator, | ||
type ConfigurableDocument, | ||
type ConfigurableNavigator | ||
} from '../../__internal__/configurable.js'; | ||
import type { AutoCleanup, CleanupFunction } from '../../__internal__/types.js'; | ||
|
||
type WakeLockType = 'screen'; | ||
|
||
interface WakeLockSentinel extends EventTarget { | ||
type: WakeLockType; | ||
released: boolean; | ||
release: () => Promise<void>; | ||
} | ||
|
||
type NavigatorWithWakeLock = Navigator & { | ||
wakeLock: { request: (type: WakeLockType) => Promise<WakeLockSentinel> }; | ||
}; | ||
|
||
interface HandleWakeLockOptions extends ConfigurableNavigator, ConfigurableDocument, AutoCleanup {} | ||
|
||
type HandleWakeLockReturn = { | ||
readonly isSupported: boolean | undefined; | ||
readonly isActive: boolean; | ||
sentinel: WakeLockSentinel | null; | ||
request: (type: WakeLockType) => Promise<void>; | ||
forceRequest: (type: WakeLockType) => Promise<void>; | ||
release: () => Promise<void>; | ||
cleanup: CleanupFunction; | ||
}; | ||
|
||
/** | ||
* Provides a way to prevent devices from dimming or locking the screen when an application needs to keep running. | ||
* @param options Additional options to customize the behavior. | ||
*/ | ||
export function handleWakeLock(options: HandleWakeLockOptions = {}): HandleWakeLockReturn { | ||
const { autoCleanup = true, navigator = defaultNavigator, document = defaultDocument } = options; | ||
|
||
let eventListenerCleanup: CleanupFunction = noop; | ||
|
||
let requestedType = $state<WakeLockType | false>(false); | ||
let sentinel = $state<WakeLockSentinel | null>(null); | ||
const documentVisibility = getDocumentVisibility({ autoCleanup, document }); | ||
const isSupported = $derived.by(() => !!navigator && 'wakeLock' in navigator); | ||
const isActive = $derived.by(() => !!sentinel && documentVisibility.current === 'visible'); | ||
|
||
if (isSupported) { | ||
eventListenerCleanup = handleEventListener( | ||
sentinel!, | ||
'release', | ||
() => { | ||
requestedType = sentinel?.type ?? false; | ||
}, | ||
{ autoCleanup, passive: true } | ||
); | ||
|
||
whenever( | ||
() => documentVisibility.current === 'visible' && !!requestedType, | ||
() => { | ||
requestedType = false; | ||
forceRequest('screen'); | ||
} | ||
); | ||
} | ||
|
||
if (autoCleanup) { | ||
onDestroy(() => cleanup()); | ||
} | ||
|
||
async function forceRequest(type: WakeLockType): Promise<void> { | ||
await sentinel?.release(); | ||
sentinel = isSupported | ||
? await (navigator as NavigatorWithWakeLock).wakeLock.request(type) | ||
: null; | ||
} | ||
|
||
async function request(type: WakeLockType): Promise<void> { | ||
if (documentVisibility.current === 'visible') { | ||
await forceRequest(type); | ||
} else { | ||
requestedType = type; | ||
} | ||
} | ||
|
||
async function release(): Promise<void> { | ||
requestedType = false; | ||
sentinel?.release().then(() => { | ||
sentinel = null; | ||
}); | ||
} | ||
|
||
function cleanup() { | ||
documentVisibility.cleanup(); | ||
eventListenerCleanup(); | ||
} | ||
|
||
return { | ||
get isSupported() { | ||
return isSupported; | ||
}, | ||
get isActive() { | ||
return isActive; | ||
}, | ||
sentinel, | ||
request, | ||
forceRequest, | ||
release, | ||
cleanup | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
packages/website/src/lib/docs/core/browser/handle-wake-lock/Demo.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<script lang="ts"> | ||
import { handleWakeLock } from '$sv-use/core'; | ||
const wakeLock = handleWakeLock(); | ||
function onclick() { | ||
return wakeLock.isActive ? wakeLock.release() : wakeLock.request('screen'); | ||
} | ||
</script> | ||
|
||
<div class="relative flex w-full flex-col gap-2"> | ||
{#if wakeLock.isSupported} | ||
<p>Is Active: {wakeLock.isActive}</p> | ||
<button {onclick} class="bg-svelte rounded-md px-3 py-1 text-white"> | ||
{wakeLock.isActive ? 'Deactivate' : 'Activate'} | ||
</button> | ||
{:else} | ||
<p>Your browser doesn't support the Screen Wake Lock API :(</p> | ||
{/if} | ||
</div> |
27 changes: 27 additions & 0 deletions
27
packages/website/src/lib/docs/core/browser/handle-wake-lock/index.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# handleWakeLock | ||
|
||
Provides a way to prevent devices from dimming or locking the screen when an application needs to keep running. | ||
|
||
You may read more about the [Screen Wake Lock API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API). | ||
|
||
## Usage | ||
|
||
> [!IMPORTANT] | ||
> Since it uses `$effect` internally, you must either call `handleWakeLock` in | ||
> the component initialization lifecycle or call it inside `$effect.root`. | ||
```svelte | ||
<script> | ||
import { handleWakeLock } from '@sv-use/core'; | ||
const wakeLock = handleWakeLock(); | ||
// When you need to prevent the screen from locking or dimming | ||
await wakeLock.request('screen'); | ||
// ... | ||
// When you don't need it anymore | ||
await wakeLock.release(); | ||
</script> | ||
``` |