mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 17:14:50 +00:00
refactor: Button components (#6929)
* feat: refactor button components
* fix: qa
* fix: storybook
* a11y: pass
* fix: build
* fix: rename default ->md
* fix: lint
* docs: buttons.md
* refactor part 1
* refactor part 2
* fix: undo refactor for fresh restart
* refactor
* Revert "refactor"
This reverts commit 96d65902d7.
* refactor: part 1
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: remove text-contrast
* fix: qa
* fix: v-tooltip
* fix: lint
* fix: split broken
* fix: passkey qa
* fix: qa
* fix: qa
* fix: prepr
* fix: splitbutton
* improve button group seam
---------
Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export * from './server-content-installing'
|
||||
export * from './server-search'
|
||||
export * from './tag-messages'
|
||||
export * from './truncate'
|
||||
export * from './use-anchored-teleport'
|
||||
export * from './version-compatibility'
|
||||
export * from './vue-children'
|
||||
export * from './webgl/skin-rendering'
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { CSSProperties, Ref } from 'vue'
|
||||
import { nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
export type AnchoredTeleportPlacement =
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
export type AnchoredTeleportSide = 'top' | 'right' | 'bottom' | 'left'
|
||||
|
||||
const viewportPadding = 8
|
||||
const anchorPadding = 18
|
||||
const defaultDistance = ref(8)
|
||||
|
||||
export function useAnchoredTeleport(
|
||||
trigger: Readonly<Ref<HTMLElement | null>>,
|
||||
panel: Readonly<Ref<HTMLElement | null>>,
|
||||
placement: Readonly<Ref<AnchoredTeleportPlacement>>,
|
||||
distance: Readonly<Ref<number>> = defaultDistance,
|
||||
) {
|
||||
const isOpen = ref(false)
|
||||
const panelStyle = ref<CSSProperties>({
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
visibility: 'hidden',
|
||||
})
|
||||
const anchorStyle = ref<CSSProperties>({})
|
||||
const resolvedSide = ref<AnchoredTeleportSide>('bottom')
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
|
||||
function updatePosition() {
|
||||
if (!isOpen.value || !trigger.value || !panel.value) return
|
||||
|
||||
const triggerRect = trigger.value.getBoundingClientRect()
|
||||
const panelRect = panel.value.getBoundingClientRect()
|
||||
const offset = distance.value
|
||||
const alignsEnd = placement.value.endsWith('end')
|
||||
const isHorizontal = placement.value.startsWith('right') || placement.value.startsWith('left')
|
||||
let idealTop: number
|
||||
let idealLeft: number
|
||||
|
||||
if (isHorizontal) {
|
||||
const prefersRight = placement.value.startsWith('right')
|
||||
const spaceRight = window.innerWidth - triggerRect.right - viewportPadding
|
||||
const spaceLeft = triggerRect.left - viewportPadding
|
||||
const opensRight = prefersRight
|
||||
? panelRect.width + offset <= spaceRight || spaceRight > spaceLeft
|
||||
: panelRect.width + offset > spaceLeft && spaceRight > spaceLeft
|
||||
|
||||
resolvedSide.value = opensRight ? 'right' : 'left'
|
||||
idealTop = alignsEnd ? triggerRect.bottom - panelRect.height : triggerRect.top
|
||||
idealLeft = opensRight
|
||||
? triggerRect.right + offset
|
||||
: triggerRect.left - panelRect.width - offset
|
||||
} else {
|
||||
const prefersTop = placement.value.startsWith('top')
|
||||
const spaceBelow = window.innerHeight - triggerRect.bottom - viewportPadding
|
||||
const spaceAbove = triggerRect.top - viewportPadding
|
||||
const opensAbove = prefersTop
|
||||
? panelRect.height + offset <= spaceAbove || spaceAbove > spaceBelow
|
||||
: panelRect.height + offset > spaceBelow && spaceAbove > spaceBelow
|
||||
|
||||
resolvedSide.value = opensAbove ? 'top' : 'bottom'
|
||||
idealTop = opensAbove
|
||||
? triggerRect.top - panelRect.height - offset
|
||||
: triggerRect.bottom + offset
|
||||
idealLeft = alignsEnd ? triggerRect.right - panelRect.width : triggerRect.left
|
||||
}
|
||||
|
||||
const maxTop = Math.max(
|
||||
viewportPadding,
|
||||
window.innerHeight - panelRect.height - viewportPadding,
|
||||
)
|
||||
const maxLeft = Math.max(viewportPadding, window.innerWidth - panelRect.width - viewportPadding)
|
||||
const panelTop = Math.min(Math.max(idealTop, viewportPadding), maxTop)
|
||||
const panelLeft = Math.min(Math.max(idealLeft, viewportPadding), maxLeft)
|
||||
|
||||
panelStyle.value = {
|
||||
top: `${panelTop}px`,
|
||||
left: `${panelLeft}px`,
|
||||
visibility: 'visible',
|
||||
}
|
||||
anchorStyle.value = isHorizontal
|
||||
? {
|
||||
top: `${Math.min(
|
||||
Math.max(triggerRect.top + triggerRect.height / 2 - panelTop, anchorPadding),
|
||||
Math.max(anchorPadding, panelRect.height - anchorPadding),
|
||||
)}px`,
|
||||
}
|
||||
: {
|
||||
left: `${Math.min(
|
||||
Math.max(triggerRect.left + triggerRect.width / 2 - panelLeft, anchorPadding),
|
||||
Math.max(anchorPadding, panelRect.width - anchorPadding),
|
||||
)}px`,
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
const target = event.target as Node | null
|
||||
if (!target || trigger.value?.contains(target) || panel.value?.contains(target)) return
|
||||
close()
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
document.addEventListener('pointerdown', handlePointerDown)
|
||||
window.addEventListener('resize', updatePosition)
|
||||
window.addEventListener('scroll', updatePosition, true)
|
||||
|
||||
resizeObserver = new ResizeObserver(updatePosition)
|
||||
if (trigger.value) resizeObserver.observe(trigger.value)
|
||||
if (panel.value) resizeObserver.observe(panel.value)
|
||||
}
|
||||
|
||||
function removeListeners() {
|
||||
document.removeEventListener('pointerdown', handlePointerDown)
|
||||
window.removeEventListener('resize', updatePosition)
|
||||
window.removeEventListener('scroll', updatePosition, true)
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = undefined
|
||||
}
|
||||
|
||||
async function open() {
|
||||
if (isOpen.value) return
|
||||
panelStyle.value = { top: '0px', left: '0px', visibility: 'hidden' }
|
||||
isOpen.value = true
|
||||
await nextTick()
|
||||
updatePosition()
|
||||
addListeners()
|
||||
}
|
||||
|
||||
function close(restoreFocus = false) {
|
||||
if (!isOpen.value) return
|
||||
isOpen.value = false
|
||||
removeListeners()
|
||||
if (restoreFocus) nextTick(() => trigger.value?.focus())
|
||||
}
|
||||
|
||||
watch([placement, distance], updatePosition)
|
||||
watch(panel, () => {
|
||||
if (!isOpen.value) return
|
||||
resizeObserver?.disconnect()
|
||||
if (trigger.value) resizeObserver?.observe(trigger.value)
|
||||
if (panel.value) resizeObserver?.observe(panel.value)
|
||||
updatePosition()
|
||||
})
|
||||
|
||||
onUnmounted(removeListeners)
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
panelStyle,
|
||||
anchorStyle,
|
||||
resolvedSide,
|
||||
open,
|
||||
close,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user