-
-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a108a52
commit 7395fab
Showing
29 changed files
with
1,371 additions
and
525 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@svelte-put/popover': major | ||
--- | ||
|
||
First complete implementation inspired by `@svelte-put/tooltip` and Popover API |
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 |
---|---|---|
|
@@ -34,7 +34,7 @@ | |
"dts-buddy": "^0.5.0", | ||
"eslint": "^8.57.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-plugin-jsdoc": "^48.4.0", | ||
"eslint-plugin-jsdoc": "^48.5.0", | ||
"eslint-plugin-svelte": "^2.41.0", | ||
"husky": "^9.0.11", | ||
"lint-staged": "^15.2.7", | ||
|
@@ -48,9 +48,9 @@ | |
"stylelint-config-html": "^1.1.0", | ||
"stylelint-config-standard": "^36.0.1", | ||
"svelte": "5.0.0-next.166", | ||
"svelte-check": "^3.8.2", | ||
"svelte-check": "^3.8.4", | ||
"tslib": "^2.6.3", | ||
"turbo": "^2.0.5", | ||
"turbo": "^2.0.6", | ||
"typescript": "^5.5.2" | ||
}, | ||
"packageManager": "[email protected]", | ||
|
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,2 @@ | ||
# Changelog | ||
|
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,47 @@ | ||
<div align="center"> | ||
|
||
# `@svelte-put/popover` | ||
|
||
[![npm.badge]][npm] [![bundlephobia.badge]][bundlephobia] [![docs.badge]][docs] | ||
|
||
Idiomatic Svelte enhancements to [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) | ||
|
||
</div> | ||
|
||
## `svelte-put` | ||
|
||
This package is part of the [@svelte-put][github.monorepo] family. For contributing guideline and more, refer to its [readme][github.monorepo]. | ||
|
||
## Usage & Documentation | ||
|
||
[See the dedicated documentation page here][docs]. | ||
|
||
## Quick Start | ||
|
||
```html | ||
<script lang="ts"> | ||
import { Popover } from '@svelte-put/popover'; | ||
WIP | ||
</script> | ||
``` | ||
|
||
## [Changelog][github.changelog] | ||
|
||
<!-- github specifics --> | ||
|
||
[github.monorepo]: https://github.com/vnphanquang/svelte-put | ||
[github.changelog]: https://github.com/vnphanquang/svelte-put/blob/main/packages/popover/CHANGELOG.md | ||
[github.issues]: https://github.com/vnphanquang/svelte-put/issues?q= | ||
|
||
<!-- heading badge --> | ||
|
||
[npm.badge]: https://img.shields.io/npm/v/@svelte-put/popover | ||
[npm]: https://www.npmjs.com/package/@svelte-put/popover | ||
[bundlephobia.badge]: https://img.shields.io/bundlephobia/minzip/@svelte-put/popover?label=minzipped | ||
[bundlephobia]: https://bundlephobia.com/package/@svelte-put/popover | ||
[repl]: https://svelte.dev/repl/9e5f9ee41c2c45aa8523993e357f6e78 | ||
[repl.badge]: https://img.shields.io/static/v1?label=&message=Svelte+REPL&logo=svelte&logoColor=fff&color=ff3e00 | ||
[docs]: https://svelte-put.vnphanquang.com/docs/popover | ||
[docs.badge]: https://img.shields.io/badge/-Docs%20Site-blue | ||
|
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,62 @@ | ||
{ | ||
"name": "@svelte-put/popover", | ||
"version": "1.0.0-next.0", | ||
"description": "Minimal and ssr-friendly enhancements to Popover API with idiomatic Svelte", | ||
"main": "src/index.js", | ||
"module": "src/index.js", | ||
"types": "types/index.d.ts", | ||
"type": "module", | ||
"exports": { | ||
".": { | ||
"types": "./types/index.d.ts", | ||
"import": "./src/index.js" | ||
} | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"files": [ | ||
"src", | ||
"types" | ||
], | ||
"scripts": { | ||
"lint": "eslint . --ignore-path=\"../../.eslintignore\"", | ||
"format": "prettier --ignore-path ../../.prettierignore --write .", | ||
"dts": "dts-buddy --write && publint", | ||
"prepublishOnly": "turbo run dts --filter=@svelte-put/popover" | ||
}, | ||
"keywords": [ | ||
"svelte", | ||
"tooltip", | ||
"progressive", | ||
"enhance", | ||
"ssr", | ||
"popover", | ||
"overlay" | ||
], | ||
"author": { | ||
"email": "[email protected]", | ||
"name": "Quang Phan", | ||
"url": "https://github.com/vnphanquang" | ||
}, | ||
"license": "MIT", | ||
"homepage": "https://svelte-put.vnphanquang.com/docs/popover", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/vnphanquang/svelte-put.git", | ||
"directory": "packages/popover" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/vnphanquang/svelte-put/issues" | ||
}, | ||
"devDependencies": { | ||
"@internals/tsconfig": "workspace:*" | ||
}, | ||
"peerDependencies": { | ||
"svelte": "^5.0.0-next.166" | ||
}, | ||
"volta": { | ||
"extends": "../../package.json" | ||
} | ||
} | ||
|
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,3 @@ | ||
export { Popover } from './popover.svelte.js'; | ||
export * from './public.js'; | ||
|
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,243 @@ | ||
import { on } from 'svelte/events'; | ||
|
||
/** | ||
* Enhance Popover API with idiomatic Svelte | ||
*/ | ||
export class Popover { | ||
/** @type {ReturnType<typeof setTimeout> | undefined} */ | ||
#hideTimeoutId = undefined; | ||
/** @type {ReturnType<typeof setTimeout> | undefined} */ | ||
#showTimeoutId = undefined; | ||
|
||
/** @type {HTMLButtonElement | null} */ | ||
#controlEl = null; | ||
|
||
/** @type {HTMLElement | null} */ | ||
#targetEl = null; | ||
|
||
// public API | ||
open = $state(false); | ||
|
||
/** @type {import('./public').PopoverConfig} */ | ||
config; | ||
/** @type {import('./public').PopoverControl} */ | ||
control; | ||
/** @type {import('./public').PopoverTarget} */ | ||
target; | ||
|
||
/** | ||
* @param {import('./public').PopoverInit} [init] | ||
*/ | ||
constructor(init = {}) { | ||
this.config = { | ||
id: init.id | ||
? init.id | ||
: // eslint-disable-next-line no-undef | ||
'crypto' in globalThis | ||
? crypto.randomUUID() | ||
: Math.random().toString(36).substring(2), | ||
inertWhenHidden: init.inertWhenHidden ?? true, | ||
triggers: { | ||
hover: { | ||
enabled: false, | ||
timeoutMs: 1000, | ||
delayMs: 0, | ||
}, | ||
focus: { | ||
enabled: false, | ||
timeoutMs: 1000, | ||
delayMs: 0, | ||
}, | ||
}, | ||
plugins: [], | ||
}; | ||
if (init.triggers) { | ||
const keys = /** @type {Array<keyof import('./public').PopoverConfig['triggers']>} */ ( | ||
Object.keys(this.config.triggers) | ||
); | ||
for (const key of keys) { | ||
if (key in init.triggers) { | ||
if (typeof init.triggers[key] === 'boolean') { | ||
this.config.triggers[key].enabled = init.triggers[key]; | ||
} else { | ||
this.config.triggers[key] = { | ||
...this.config.triggers[key], | ||
...init.triggers[key], | ||
}; | ||
} | ||
} | ||
} | ||
} | ||
|
||
this.config.plugins = init.plugins | ||
? Array.isArray(init.plugins) | ||
? init.plugins | ||
: [init.plugins] | ||
: []; | ||
|
||
const rPlugins = this.config.plugins.map(p => p(this.config)); | ||
const plugined = { | ||
control: { | ||
attributes: rPlugins | ||
.map((p) => p.control?.attributes ?? {}) | ||
.reduce((acc, val) => ({ ...acc, ...val }), {}), | ||
actions: rPlugins.map((p) => p.control?.actions ?? []).flat(), | ||
}, | ||
target: { | ||
attributes: rPlugins | ||
.map((p) => p.target?.attributes ?? {}) | ||
.reduce((acc, val) => ({ ...acc, ...val }), {}), | ||
actions: rPlugins.map((p) => p.target?.actions ?? []).flat(), | ||
}, | ||
}; | ||
|
||
this.control = { | ||
attributes: { | ||
popovertarget: this.config.id, | ||
popovertargetaction: 'show', | ||
...(plugined.control?.attributes ?? {}), | ||
}, | ||
actions: (node) => { | ||
this.#controlEl = node; | ||
|
||
/** @type {Array<() => void>} */ | ||
const offs = []; | ||
|
||
if (this.config.triggers.focus.enabled) { | ||
offs.push( | ||
on(node, 'focusin', () => this.show(this.config.triggers.focus.delayMs)), | ||
on(node, 'focusout', () => this.hide(this.config.triggers.focus.timeoutMs)), | ||
); | ||
} | ||
|
||
if (this.config.triggers.hover.enabled) { | ||
offs.push( | ||
on(node, 'mouseenter', () => this.show(this.config.triggers.focus.delayMs)), | ||
on(node, 'mouseleave', () => this.hide(this.config.triggers.focus.timeoutMs)), | ||
); | ||
} | ||
|
||
// additional actions | ||
const actions = plugined.control?.actions ?? []; | ||
const actionReturns = actions.map((action) => action(node, this)); | ||
|
||
return { | ||
update: () => { | ||
actionReturns.forEach((actionReturn) => actionReturn?.update?.(this)); | ||
}, | ||
destroy: () => { | ||
offs.forEach((off) => off()); | ||
actionReturns.forEach((actionReturn) => actionReturn?.destroy?.()); | ||
}, | ||
}; | ||
}, | ||
}; | ||
|
||
this.target = { | ||
attributes: { | ||
popover: 'auto', | ||
id: this.config.id, | ||
...(plugined.target?.attributes ?? {}), | ||
}, | ||
actions: (node) => { | ||
this.#targetEl = node; | ||
|
||
$effect.root(() => { | ||
$effect(() => { | ||
// make popover inert when not open | ||
node.inert = !this.open; | ||
}); | ||
}); | ||
|
||
const offs = [ | ||
on(node, 'toggle', (e) => { | ||
this.open = /** @type {ToggleEvent} */ (e).newState === 'open'; | ||
}), | ||
]; | ||
|
||
if (this.config.triggers.focus.enabled) { | ||
offs.push( | ||
on(node, 'focusin', () => this.open && this.show(this.config.triggers.focus.delayMs)), | ||
on( | ||
node, | ||
'focusout', | ||
() => this.open && this.hide(this.config.triggers.focus.timeoutMs), | ||
), | ||
); | ||
} | ||
|
||
if (this.config.triggers.hover.enabled) { | ||
offs.push( | ||
/** | ||
* For touch devices, popover is opened by pressing on the control element (button) | ||
* which gives it focus. So when the target element is touched within, control element | ||
* loses focus, causing the `focusout` event to fire AFTER `touchstart`. The `setTimeout` | ||
* call is to work around this. | ||
*/ | ||
on( | ||
node, | ||
'touchstart', | ||
() => this.open && this.show(this.config.triggers.focus.delayMs + 100), | ||
), | ||
on( | ||
node, | ||
'mouseenter', | ||
() => this.open && this.show(this.config.triggers.focus.delayMs), | ||
), | ||
on( | ||
node, | ||
'mouseleave', | ||
() => this.open && this.hide(this.config.triggers.focus.timeoutMs), | ||
), | ||
); | ||
} | ||
|
||
// additional actions | ||
const actions = plugined.target?.actions ?? []; | ||
const actionReturns = actions.map((action) => action(node, this)); | ||
|
||
return { | ||
update: () => { | ||
actionReturns.forEach((actionReturn) => actionReturn?.update?.(this)); | ||
}, | ||
destroy: () => { | ||
offs.forEach((off) => off()); | ||
actionReturns.forEach((actionReturn) => actionReturn?.destroy?.()); | ||
}, | ||
}; | ||
}, | ||
}; | ||
} | ||
|
||
/** | ||
* @param {number} [ms] - milliseconds to delay until showing | ||
*/ | ||
show = (ms = 0) => { | ||
clearTimeout(this.#hideTimeoutId); | ||
clearTimeout(this.#showTimeoutId); | ||
if (this.open) return; | ||
this.#showTimeoutId = setTimeout(() => { | ||
this.#showTimeoutId = undefined; | ||
this.#targetEl?.showPopover(); | ||
}, ms); | ||
}; | ||
|
||
/** | ||
* @param {number} [ms] - milliseconds to delay until hiding | ||
*/ | ||
hide = (ms = 0) => { | ||
clearTimeout(this.#hideTimeoutId); | ||
clearTimeout(this.#showTimeoutId); | ||
if (!this.open) return; | ||
this.#hideTimeoutId = setTimeout(() => { | ||
this.#hideTimeoutId = undefined; | ||
this.#targetEl?.hidePopover(); | ||
}, ms); | ||
}; | ||
|
||
toggle = () => { | ||
if (this.open) return this.hide(); | ||
this.show(); | ||
}; | ||
} | ||
|
Oops, something went wrong.