diff --git a/index.html b/index.html index 249545af0..63f8eb2d3 100644 --- a/index.html +++ b/index.html @@ -5,292 +5,23 @@ --> + -
- - - - - - - - - +




+
+ + +
+ Tooltip contents +
- - - - - - - - - - - - - - - - -
- - - -

-
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
- - - -
- -
- Yo: -
- - - -
-
Query:
- -
- - Empty - - - - -
-
- -
-
    - - - -
-
-
-
- - - - - - -
-
- - -
-
- - -
- -
-
    - -
- -

No frameworks match your query.

-
-
-
local selected:
-
internal selected:
-
-
-
- - - - +




diff --git a/package-lock.json b/package-lock.json index 0b262f37b..ec2d072b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "workspaces": [ "packages/*" ], + "dependencies": { + "@floating-ui/dom": "^1.5.3" + }, "devDependencies": { "axios": "^0.21.1", "chalk": "^4.1.1", @@ -17,6 +20,10 @@ "jest": "^26.6.3" } }, + "node_modules/@alpinejs/anchor": { + "resolved": "packages/anchor", + "link": true + }, "node_modules/@alpinejs/collapse": { "resolved": "packages/collapse", "link": true @@ -1014,6 +1021,28 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -7825,15 +7854,19 @@ } }, "packages/alpinejs": { - "version": "3.13.1", + "version": "3.13.2", "license": "MIT", "dependencies": { "@vue/reactivity": "~3.1.1" } }, + "packages/anchor": { + "version": "3.13.2", + "license": "MIT" + }, "packages/collapse": { "name": "@alpinejs/collapse", - "version": "3.13.1", + "version": "3.13.2", "license": "MIT" }, "packages/csp": { @@ -7846,12 +7879,12 @@ }, "packages/docs": { "name": "@alpinejs/docs", - "version": "3.13.1-revision.1", + "version": "3.13.2-revision.1", "license": "MIT" }, "packages/focus": { "name": "@alpinejs/focus", - "version": "3.13.1", + "version": "3.13.2", "license": "MIT", "dependencies": { "focus-trap": "^6.9.4", @@ -7868,17 +7901,17 @@ }, "packages/intersect": { "name": "@alpinejs/intersect", - "version": "3.13.1", + "version": "3.13.2", "license": "MIT" }, "packages/mask": { "name": "@alpinejs/mask", - "version": "3.13.1", + "version": "3.13.2", "license": "MIT" }, "packages/morph": { "name": "@alpinejs/morph", - "version": "3.13.1", + "version": "3.13.2", "license": "MIT" }, "packages/navigate": { @@ -7891,7 +7924,7 @@ }, "packages/persist": { "name": "@alpinejs/persist", - "version": "3.13.1", + "version": "3.13.2", "license": "MIT" }, "packages/ui": { diff --git a/package.json b/package.json index 59d17f040..c8de4637e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "chalk": "^4.1.1", "cypress": "^7.0.0", "cypress-plugin-tab": "^1.0.5", + "@floating-ui/dom": "^1.5.3", "dot-json": "^1.2.2", "esbuild": "~0.16.17", "jest": "^26.6.3" diff --git a/packages/alpinejs/src/directives.js b/packages/alpinejs/src/directives.js index 691721236..58eb09bcf 100644 --- a/packages/alpinejs/src/directives.js +++ b/packages/alpinejs/src/directives.js @@ -203,6 +203,7 @@ let directiveOrder = [ 'ref', 'data', 'id', + 'anchor', 'bind', 'init', 'for', diff --git a/packages/anchor/builds/cdn.js b/packages/anchor/builds/cdn.js new file mode 100644 index 000000000..cd138feb4 --- /dev/null +++ b/packages/anchor/builds/cdn.js @@ -0,0 +1,5 @@ +import anchor from '../src/index.js' + +document.addEventListener('alpine:init', () => { + window.Alpine.plugin(anchor) +}) diff --git a/packages/anchor/builds/module.js b/packages/anchor/builds/module.js new file mode 100644 index 000000000..2206b98a5 --- /dev/null +++ b/packages/anchor/builds/module.js @@ -0,0 +1,3 @@ +import anchor from '../src/index.js' + +export default anchor diff --git a/packages/anchor/package.json b/packages/anchor/package.json new file mode 100644 index 000000000..1e32fc1ed --- /dev/null +++ b/packages/anchor/package.json @@ -0,0 +1,17 @@ +{ + "name": "@alpinejs/anchor", + "version": "3.13.2", + "description": "Anchor an element's position relative to another", + "homepage": "https://alpinejs.dev/plugins/anchor", + "repository": { + "type": "git", + "url": "https://github.com/alpinejs/alpine.git", + "directory": "packages/anchor" + }, + "author": "Caleb Porzio", + "license": "MIT", + "main": "dist/module.cjs.js", + "module": "dist/module.esm.js", + "unpkg": "dist/cdn.min.js", + "dependencies": {} +} diff --git a/packages/anchor/src/index.js b/packages/anchor/src/index.js new file mode 100644 index 000000000..ce35ec368 --- /dev/null +++ b/packages/anchor/src/index.js @@ -0,0 +1,57 @@ +import { computePosition, autoUpdate, flip, offset, shift } from '@floating-ui/dom' + +export default function (Alpine) { + Alpine.magic('anchor', el => { + if (! el._x_anchor) throw 'Alpine: No x-anchor directive found on element using $anchor...' + + return el._x_anchor + }) + + Alpine.directive('anchor', (el, { expression, modifiers, value }, { cleanup, evaluate }) => { + el._x_anchor = Alpine.reactive({ x: 0, y: 0 }) + + let reference = evaluate(expression) + + if (! reference) throw 'Alpine: no element provided to x-anchor...' + + let positions = ['top', 'top-start', 'top-end', 'right', 'right-start', 'right-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end'] + let placement = positions.find(i => modifiers.includes(i)) + + let offsetValue = 0 + + let unstyled = modifiers.includes('unstyled') + + if (modifiers.includes('offset')) { + let idx = modifiers.findIndex(i => i === 'offset') + + offsetValue = modifiers[idx + 1] !== undefined ? Number(modifiers[idx + 1]) : offsetValue + } + + let release = autoUpdate(reference, el, () => { + let previousValue + + computePosition(reference, el, { + placement, + middleware: [flip(), shift({padding: 5}), offset(offsetValue)], + }).then(({ x, y }) => { + // Only trigger Alpine reactivity when the value actually changes... + if (JSON.stringify({ x, y }) !== previousValue) { + unstyled || setStyles(el, x, y) + + el._x_anchor.x = x + el._x_anchor.y = y + } + + previousValue = JSON.stringify({ x, y }) + }) + }) + + cleanup(() => release()) + }) +} + +function setStyles(el, x, y) { + Object.assign(el.style, { + left: x+'px', top: y+'px', position: 'absolute', + }) +} diff --git a/packages/docs/src/en/plugins/anchor.md b/packages/docs/src/en/plugins/anchor.md new file mode 100644 index 000000000..621ab58ff --- /dev/null +++ b/packages/docs/src/en/plugins/anchor.md @@ -0,0 +1,183 @@ +--- +order: 5 +title: Anchor +description: Anchor an element's positioning to another element on the pageg +graph_image: https://alpinejs.dev/social_anchor.jpg +--- + +# Anchor Plugin + +Alpine's Anchor plugin allows you easily anchor an element's positioning to another element on the page. + +This functionality is useful when creating dropdown menus, popovers, dialogs, and tooltips with Alpine. + +The "anchoring" functionality used in this plugin is provided by the [Floating UI](https://floating-ui.com/) project. + + +## Installation + +You can use this plugin by either including it from a ` + + + +``` + +### Via NPM + +You can install Collapse from NPM for use inside your bundle like so: + +```shell +npm install @alpinejs/anchor +``` + +Then initialize it from your bundle: + +```js +import Alpine from 'alpinejs' +import anchor from '@alpinejs/anchor' + +Alpine.plugin(anchor) + +... +``` + + +## x-anchor + +The primary API for using this plugin is the `x-anchor` directive. + +To use this plugin, add the `x-anchor` directive to any element and pass it a reference to the element you want to anchor it's position to (often a button on the page). + +By default, `x-anchor` will set the the element's CSS to `position: absolute` and the appropriate `top` and `left` values. If the anchored element is normally displayed below the reference element but doesn't have room on the page, it's styling will be adjusted to render above the element. + +For example, here's a simple dropdown anchored to the button that toggles it: + +```alpine +
+ + +
+ Dropdown content +
+
+``` + + +
+
+ +
+ +
+ Dropdown content +
+
+ + + +## Positioning + +`x-anchor` allows you to customize the positioning of the anchored element using the following modifiers: + +* Bottom: `.bottom`, `.bottom-start`, `.bottom-end` +* Top: `.top`, `.top-start`, `.top-end` +* Left: `.left`, `.left-start`, `.left-end` +* Right: `.right`, `.right-start`, `.right-end` + +Here is an example of using `.bottom-start` to position a dropdown below and to the right of the reference element: + +```alpine +
+ + +
+ Dropdown content +
+
+``` + + +
+
+ +
+ +
+ Dropdown content +
+
+ + + +## Offset + +You can add an offset to your anchored element using the `.offset.[px value]` modifier like so: + +```alpine +
+ + +
+ Dropdown content +
+
+``` + + +
+
+ +
+ +
+ Dropdown content +
+
+ + + +## Manual styling + +By default, `x-alpine` applies the positioning styles to your element under the hood. If you'd prefer full control over styling, you can pass the `.unstyled` modifer and use the `$anchor` magic to access the values inside another Alpine expression. + +Below is an example of bypassing `x-anchor`'s internal styling and instead applying the styles yourself using `x-bind:style`: + +```alpine +
+ + +
+ Dropdown content +
+
+``` + + +
+
+ +
+ +
+ Dropdown content +
+
+ + diff --git a/packages/docs/src/en/plugins/morph.md b/packages/docs/src/en/plugins/morph.md index 135a4590f..b8a8769af 100644 --- a/packages/docs/src/en/plugins/morph.md +++ b/packages/docs/src/en/plugins/morph.md @@ -1,5 +1,5 @@ --- -order: 5 +order: 6 title: Morph description: Morph an element into the provided HTML graph_image: https://alpinejs.dev/social_morph.jpg diff --git a/packages/ui/src/menu.js b/packages/ui/src/menu.js index 900769260..3b493f38d 100644 --- a/packages/ui/src/menu.js +++ b/packages/ui/src/menu.js @@ -30,7 +30,6 @@ function handleRoot(el, Alpine) { __activeEl: null, __isOpen: false, __open(activationStrategy) { - this.__isOpen = true // Safari needs more of a "tick" for focusing after x-show for some reason. diff --git a/scripts/build.js b/scripts/build.js index 316d8d2bb..9a1c58925 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -11,6 +11,7 @@ let zlib = require('zlib'); 'intersect', 'persist', 'collapse', + 'anchor', 'morph', 'focus', 'mask', diff --git a/tests/cypress/integration/plugins/anchor.spec.js b/tests/cypress/integration/plugins/anchor.spec.js new file mode 100644 index 000000000..52e97eeb5 --- /dev/null +++ b/tests/cypress/integration/plugins/anchor.spec.js @@ -0,0 +1,13 @@ +import { haveAttribute, haveComputedStyle, html, notHaveAttribute, test } from '../../utils' + +test('can anchor an element', + [html` +
+ +

contents

+
+ `], + ({ get }, reload) => { + get('h1').should(haveComputedStyle('position', 'absolute')) + }, +) diff --git a/tests/cypress/spec.html b/tests/cypress/spec.html index 815fde5e4..4f9456d20 100644 --- a/tests/cypress/spec.html +++ b/tests/cypress/spec.html @@ -11,6 +11,7 @@ +