Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo-mod-jslibs-dist.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'alpinejs/packages/ui/src/popover.js')
-rw-r--r--alpinejs/packages/ui/src/popover.js191
1 files changed, 191 insertions, 0 deletions
diff --git a/alpinejs/packages/ui/src/popover.js b/alpinejs/packages/ui/src/popover.js
new file mode 100644
index 0000000..889efea
--- /dev/null
+++ b/alpinejs/packages/ui/src/popover.js
@@ -0,0 +1,191 @@
+
+export default function (Alpine) {
+ Alpine.directive('popover', (el, directive) => {
+ if (! directive.value) handleRoot(el, Alpine)
+ else if (directive.value === 'overlay') handleOverlay(el, Alpine)
+ else if (directive.value === 'button') handleButton(el, Alpine)
+ else if (directive.value === 'panel') handlePanel(el, Alpine)
+ else if (directive.value === 'group') handleGroup(el, Alpine)
+ })
+
+ Alpine.magic('popover', el => {
+ let $data = Alpine.$data(el)
+
+ return {
+ get open() {
+ return $data.__isOpen
+ }
+ }
+ })
+}
+
+function handleRoot(el, Alpine) {
+ Alpine.bind(el, {
+ 'x-id'() { return ['alpine-popover-button', 'alpine-popover-panel'] },
+ 'x-data'() {
+ return {
+ init() {
+ if (this.$data.__groupEl) {
+ this.$data.__groupEl.addEventListener('__close-others', ({ detail }) => {
+ if (detail.el.isSameNode(this.$el)) return
+
+ this.__close(false)
+ })
+ }
+ },
+ __buttonEl: undefined,
+ __panelEl: undefined,
+ __isOpen: false,
+ __open() {
+ this.__isOpen = true
+
+ this.$dispatch('__close-others', { el: this.$el })
+ },
+ __toggle() {
+ this.__isOpen ? this.__close() : this.__open()
+ },
+ __close(el) {
+ this.__isOpen = false
+
+ if (el === false) return
+
+ el = el || this.$data.__buttonEl
+
+ if (document.activeElement.isSameNode(el)) return
+
+ setTimeout(() => el.focus())
+ },
+ __contains(outer, inner) {
+ return !! Alpine.findClosest(inner, el => el.isSameNode(outer))
+ }
+ }
+ },
+ '@keydown.escape.stop.prevent'() {
+ this.__close()
+ },
+ '@focusin.window'() {
+ if (this.$data.__groupEl) {
+ if (! this.$data.__contains(this.$data.__groupEl, document.activeElement)) {
+ this.$data.__close(false)
+ }
+
+ return
+ }
+
+ if (! this.$data.__contains(this.$el, document.activeElement)) {
+ this.$data.__close(false)
+ }
+ },
+ })
+}
+
+function handleButton(el, Alpine) {
+ Alpine.bind(el, {
+ 'x-ref': 'button',
+ ':id'() { return this.$id('alpine-popover-button') },
+ ':aria-expanded'() { return this.$data.__isOpen },
+ ':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-popover-panel') },
+ 'x-init'() {
+ if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
+
+ this.$data.__buttonEl = this.$el
+ },
+ '@click'() { this.$data.__toggle() },
+ '@keydown.tab'(e) {
+ if (! e.shiftKey && this.$data.__isOpen) {
+ let firstFocusableEl = this.$focus.within(this.$data.__panelEl).getFirst()
+
+ if (firstFocusableEl) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ this.$focus.focus(firstFocusableEl)
+ }
+ }
+ },
+ '@keyup.tab'(e) {
+ if (this.$data.__isOpen) {
+ // Check if the last focused element was "after" this one
+ let lastEl = this.$focus.previouslyFocused()
+
+ if (! lastEl) return
+
+ if (
+ // Make sure the last focused wasn't part of this popover.
+ (! this.$data.__buttonEl.contains(lastEl) && ! this.$data.__panelEl.contains(lastEl))
+ // Also make sure it appeared "after" this button in the DOM.
+ && (lastEl && (this.$el.compareDocumentPosition(lastEl) & Node.DOCUMENT_POSITION_FOLLOWING))
+ ) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ this.$focus.within(this.$data.__panelEl).last()
+ }
+ }
+ },
+ '@keydown.space.stop.prevent'() { this.$data.__toggle() },
+ '@keydown.enter.stop.prevent'() { this.$data.__toggle() },
+ // This is to stop Firefox from firing a "click".
+ '@keyup.space.stop.prevent'() { },
+ })
+}
+
+function handlePanel(el, Alpine) {
+ Alpine.bind(el, {
+ 'x-init'() { this.$data.__panelEl = this.$el },
+ 'x-effect'() {
+ this.$data.__isOpen && Alpine.bound(el, 'focus') && this.$focus.first()
+ },
+ 'x-ref': 'panel',
+ ':id'() { return this.$id('alpine-popover-panel') },
+ 'x-show'() { return this.$data.__isOpen },
+ '@mousedown.window'($event) {
+ if (! this.$data.__isOpen) return
+ if (this.$data.__contains(this.$data.__buttonEl, $event.target)) return
+ if (this.$data.__contains(this.$el, $event.target)) return
+
+ if (! this.$focus.focusable($event.target)) {
+ this.$data.__close()
+ }
+ },
+ '@keydown.tab'(e) {
+ if (e.shiftKey && this.$focus.isFirst(e.target)) {
+ e.preventDefault()
+ e.stopPropagation()
+ Alpine.bound(el, 'focus') ? this.$data.__close() : this.$data.__buttonEl.focus()
+ } else if (! e.shiftKey && this.$focus.isLast(e.target)) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ // Get the next panel button:
+ let els = this.$focus.within(document).all()
+ let buttonIdx = els.indexOf(this.$data.__buttonEl)
+
+ let nextEls = els
+ .splice(buttonIdx + 1) // Elements after button
+ .filter(el => ! this.$el.contains(el)) // Ignore items in panel
+
+ nextEls[0].focus()
+
+ Alpine.bound(el, 'focus') && this.$data.__close(false)
+ }
+ },
+ })
+}
+
+function handleGroup(el, Alpine) {
+ Alpine.bind(el, {
+ 'x-ref': 'container',
+ 'x-data'() {
+ return {
+ __groupEl: this.$el,
+ }
+ },
+ })
+}
+
+function handleOverlay(el, Alpine) {
+ Alpine.bind(el, {
+ 'x-show'() { return this.$data.__isOpen }
+ })
+}