Caleb Porzio 3 anos atrás
pai
commit
29264cacad

+ 7 - 24
index.html

@@ -7,31 +7,14 @@
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
     <!-- <script src="https://unpkg.com/alpinejs@3.0.0/dist/cdn.min.js" defer></script> -->
 
+    <div x-data>
+        <label :for="$id('text-input')">...</label>
+    
+        <input type="text" :id="$id('text-input')">
+    </div>
 
-    <!-- Add aria-hidden="true" to all other elements. -->
-    <div x-trap.inert="open">...</div>
-
-    <!-- Disabled scrolling when a dialog is open. -->
-    <div x-trap.noscroll="open">...</div>
-
-    <!-- Easily perist global store data in localStorage. -->
-    <script>
-        Alpine.store('config', {
-            darkMode: Alpine.$persist(false),
-        })
-    </script>
-
-    <!-- You can now pass around Alpine scope as an object with the new $data magic. -->
-    <div @click="doSomething($data)"></div>
-
-    <!-- x-model data can now be programmatically "get" and "set" (with reactivity). -->
-    <div
-        x-model="open"
-        x-text="$el._x_model.get()"
-        @click="$el._x_model.set('...')"
-    >...</div>
-     
-
+    <div :id="$id('modal-title').refreshScope()"></div>
+    
 
     <!-- Play around. -->
     <div x-data="{ open: false }">

+ 10 - 4
packages/alpinejs/src/lifecycle.js

@@ -45,15 +45,21 @@ export function addRootSelector(selectorCallback) { rootSelectorCallbacks.push(s
 export function addInitSelector(selectorCallback) { initSelectorCallbacks.push(selectorCallback) }
 
 export function closestRoot(el, includeInitSelectors = false) {
-    if (!el) return
+    return findClosest(el, element => {
+        const selectors = includeInitSelectors ? allSelectors() : rootSelectors()
 
-    const selectors = includeInitSelectors ? allSelectors() : rootSelectors()
+        if (selectors.some(selector => element.matches(selector))) return true
+    }) 
+}
+
+export function findClosest(el, callback) {
+    if (! el) return
 
-    if (selectors.some(selector => el.matches(selector))) return el
+    if (callback(el)) return el
 
     if (! el.parentElement) return
 
-    return closestRoot(el.parentElement, includeInitSelectors)
+    return findClosest(el.parentElement, callback)
 }
 
 export function isRoot(el) {

+ 42 - 27
packages/alpinejs/src/magics/$id.js

@@ -1,40 +1,55 @@
-import { closestDataStack, mergeProxies } from '../scope'
 import { magic } from '../magics'
-import { closestRoot } from '../lifecycle'
-
-let memo = {}
-
-magic('id', el => (name, key = null) => {
-    if (! memo[name]) memo[name] = 0
-
-    let root = closestRoot(el)
-    let id = getId(root, el, name)
+import { directive } from '../directives'
+import { findClosest, closestRoot } from '../lifecycle'
+
+let globalIdMemo = {}
+
+function generateIdMagicFunction(el) {
+    function idMagic(name, key = null) {
+        if (! globalIdMemo[name]) globalIdMemo[name] = 0
+    
+        let id = getId(el, name)
+    
+        return key
+            ? new HtmlId(el, `${name}-${id}-${key}`)
+            : new HtmlId(el, `${name}-${id}`)
+    }
 
-    if (key) {
-        return `${name}-${id}-${key}`
+    idMagic.scope = function () {
+        el._x_new_scope = true
     }
 
-    return `${name}-${id}`
-})
+    return idMagic
+}
+
+magic('id', el => generateIdMagicFunction(el))
 
-function getId(root, el, name) {
-    if (! root._x_ids) root._x_ids = []
+function getId(el, name) {
+    let root = closestIdRoot(el, name) || closestRoot(el)
 
-    if (root._x_ids.includes(name)) {
-        return memo[name]
-    } else {
-        root._x_ids.push(name) 
+    initRoot(root, name)
         
-        return ++memo[name]
-    }
+    return root._x_ids[name]
 }
 
-export function closestIdRoot(el) {
-    if (! el) return
+class HtmlId {
+    constructor(el, id) { this.id = id }
+    toString() { return this.id }
+}
 
-    if (el._x_ids) return el
+export function closestIdRoot(el, name) {
+    return findClosest(el, element => {
+        if (element._x_new_scope) {
+            initRoot(element, name)
 
-    if (! el.parentElement) return
+            return true
+        }
+
+        if (element._x_ids && element._x_ids[name]) return true
+    })
+}
 
-    return closestIdRoot(el.parentElement)
+function initRoot(el, name) {
+    if (! el._x_ids) el._x_ids = {}
+    if (! el._x_ids[name]) el._x_ids[name] = ++globalIdMemo[name]
 }

+ 59 - 2
tests/cypress/integration/magics/$id.spec.js

@@ -3,11 +3,11 @@ import { haveAttribute, haveText, html, test } from '../../utils'
 test('$id generates a unique id',
     html`
         <div x-data id="1">
-            <span :aria-labelledby="$id('foo')"></span>
-
             <div>
                 <h1 :id="$id('foo')"></h1>
             </div>
+
+            <span :aria-labelledby="$id('foo')"></span>
         </div>
 
         <div x-data id="2">
@@ -55,5 +55,62 @@ test('$id works with keys and nested data scopes',
         get('#1 li:nth-child(1)').should(haveAttribute('id', 'foo-1-1'))
         get('#1 li:nth-child(2)').should(haveAttribute('id', 'foo-1-2'))
         get('#1 li:nth-child(3)').should(haveAttribute('id', 'foo-1-3'))
+        get('#2 span').should(haveAttribute('aria-activedescendant', 'foo-2-3'))
+        get('#2 li:nth-child(1)').should(haveAttribute('id', 'foo-2-1'))
+        get('#2 li:nth-child(2)').should(haveAttribute('id', 'foo-2-2'))
+        get('#2 li:nth-child(3)').should(haveAttribute('id', 'foo-2-3'))
+    }
+)
+
+test('$id scopes are grouped by name',
+    html`
+        <div x-data>
+            <!-- foo-1 -->
+            <span :aria-activedescendant="$id('foo')"></span>
+
+            <ul>
+                <li x-data :id="$id('bar')"></li> <!-- bar-1 -->
+                <li x-data :id="$id('bar')"></li> <!-- bar-2 -->
+            </ul>
+        </div>
+    `,
+    ({ get }) => {
+        get('span').should(haveAttribute('aria-activedescendant', 'foo-1'))
+        get('li:nth-child(1)').should(haveAttribute('id', 'bar-1'))
+        get('li:nth-child(2)').should(haveAttribute('id', 'bar-2'))
+    }
+)
+
+test('$id scopes can be reset',
+    html`
+        <div x-data>
+            <!-- foo-1 -->
+            <span :aria-labelledby="$id('foo')"></span>
+
+            <div x-data>
+                <h1 :id="$id('foo')"></h1>
+                <h5 :id="$id('bar')"></h5>
+                
+                <div x-init="$id.scope('foo')">
+                    <h2 :aria-labelledby="$id('foo')"></h2>
+                    <h6 :aria-labelledby="$id('bar')"></h6>
+
+                    <div x-data>
+                        <h3 :id="$id('foo')"></h3>
+                    </div>
+                </div>
+
+                <h4 :id="$id('foo')"></h4>
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('span').should(haveAttribute('aria-labelledby', 'foo-1'))
+        get('h1').should(haveAttribute('id', 'foo-1'))
+        get('h2').should(haveAttribute('aria-labelledby', 'foo-2'))
+        get('h3').should(haveAttribute('id', 'foo-2'))
+        get('h4').should(haveAttribute('id', 'foo-1'))
+        get('h5').should(haveAttribute('id', 'bar-1'))
+        get('h6').should(haveAttribute('aria-labelledby', 'bar-1'))
     }
 )