still-popper

This commit is contained in:
Untone 2024-10-09 18:52:30 +03:00
parent 16da6e94cf
commit 7a4e275b0b
6 changed files with 169 additions and 205 deletions

116
package-lock.json generated
View File

@ -11,7 +11,8 @@
"dependencies": {
"form-data": "^4.0.0",
"idb": "^8.0.0",
"mailgun.js": "^10.2.3"
"mailgun.js": "^10.2.3",
"solid-popper": "0.3.0"
},
"devDependencies": {
"@authorizerdev/authorizer-js": "^2.0.3",
@ -93,13 +94,11 @@
"javascript-time-ago": "^2.5.11",
"patch-package": "^8.0.0",
"prosemirror-history": "^1.4.1",
"prosemirror-trailing-node": "^2.0.9",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-view": "^1.34.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.4",
"solid-js": "^1.9.2",
"solid-tiptap": "0.7.0",
"solid-transition-group": "^0.2.3",
"storybook": "^8.3.5",
"storybook-addon-sass-postcss": "^0.3.2",
"storybook-solidjs": "^1.0.0-beta.2",
@ -4871,7 +4870,6 @@
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
@ -4879,9 +4877,9 @@
}
},
"node_modules/@remirror/core-constants": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz",
"integrity": "sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
"dev": true,
"license": "MIT"
},
@ -5461,19 +5459,6 @@
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/refs": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.0.8.tgz",
"integrity": "sha512-+jIsWG8/nYvhaCoG2Vg6CJOLgTmPKFbaCrNQKWfChalgUf9WrVxWw0CdJb3yX15n5lUcQ0jBo6qYtuVVmBLpBw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/utils": "^6.2.3"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/rootless": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.4.5.tgz",
@ -5552,16 +5537,6 @@
}
}
},
"node_modules/@solid-primitives/transition-group": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@solid-primitives/transition-group/-/transition-group-1.0.5.tgz",
"integrity": "sha512-G3FuqvL13kQ55WzWPX2ewiXdZ/1iboiX53195sq7bbkDbXqP6TYKiadwEdsaDogW5rPnPYAym3+xnsNplQJRKQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/@solid-primitives/upload": {
"version": "0.0.117",
"resolved": "https://registry.npmjs.org/@solid-primitives/upload/-/upload-0.0.117.tgz",
@ -7236,42 +7211,6 @@
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/pm/node_modules/@remirror/core-constants": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
"dev": true,
"license": "MIT"
},
"node_modules/@tiptap/pm/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@tiptap/pm/node_modules/prosemirror-trailing-node": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@remirror/core-constants": "3.0.0",
"escape-string-regexp": "^4.0.0"
},
"peerDependencies": {
"prosemirror-model": "^1.22.1",
"prosemirror-state": "^1.4.2",
"prosemirror-view": "^1.33.8"
}
},
"node_modules/@tiptap/starter-kit": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.8.0.tgz",
@ -11152,7 +11091,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT"
},
"node_modules/cwd": {
@ -20882,13 +20820,13 @@
}
},
"node_modules/prosemirror-trailing-node": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz",
"integrity": "sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@remirror/core-constants": "^2.0.2",
"@remirror/core-constants": "3.0.0",
"escape-string-regexp": "^4.0.0"
},
"peerDependencies": {
@ -22412,7 +22350,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.1.1.tgz",
"integrity": "sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -22422,7 +22359,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.1.1.tgz",
"integrity": "sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -22665,7 +22601,6 @@
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.2.tgz",
"integrity": "sha512-fe/K03nV+kMFJYhAOE8AIQHcGxB4rMIEoEyrulbtmf217NffbbwBqJnJI4ovt16e+kaIt0czE2WA7mP/pYN9yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.1.0",
@ -22673,6 +22608,19 @@
"seroval-plugins": "^1.1.0"
}
},
"node_modules/solid-popper": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/solid-popper/-/solid-popper-0.3.0.tgz",
"integrity": "sha512-XlfEWAyxGGqFgg/uRpF+BemSfCqjbLA8p6fToDa+6v3paw3eBQj0TU08aBOIj2VeigaEiz8ZTlDx1eBLVRivBg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@popperjs/core": "^2.11",
"solid-js": "^1.2"
}
},
"node_modules/solid-refresh": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz",
@ -22703,24 +22651,6 @@
"solid-js": "^1.7"
}
},
"node_modules/solid-transition-group": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/solid-transition-group/-/solid-transition-group-0.2.3.tgz",
"integrity": "sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@solid-primitives/refs": "^1.0.5",
"@solid-primitives/transition-group": "^1.0.2"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.6.0"
},
"peerDependencies": {
"solid-js": "^1.6.12"
}
},
"node_modules/solid-use": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/solid-use/-/solid-use-0.9.0.tgz",

View File

@ -100,13 +100,11 @@
"javascript-time-ago": "^2.5.11",
"patch-package": "^8.0.0",
"prosemirror-history": "^1.4.1",
"prosemirror-trailing-node": "^2.0.9",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-view": "^1.34.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.79.4",
"solid-js": "^1.9.2",
"solid-tiptap": "0.7.0",
"solid-transition-group": "^0.2.3",
"storybook": "^8.3.5",
"storybook-addon-sass-postcss": "^0.3.2",
"storybook-solidjs": "^1.0.0-beta.2",
@ -133,10 +131,16 @@
"yjs": "13.6.19",
"y-prosemirror": "1.2.12"
},
"trustedDependencies": ["@biomejs/biome", "@swc/core", "esbuild", "protobufjs"],
"trustedDependencies": [
"@biomejs/biome",
"@swc/core",
"esbuild",
"protobufjs"
],
"dependencies": {
"form-data": "^4.0.0",
"idb": "^8.0.0",
"mailgun.js": "^10.2.3"
"mailgun.js": "^10.2.3",
"solid-popper": "0.3.0"
}
}

View File

@ -1,39 +1,40 @@
.snackbar {
background-color: var(--default-color);
color: #fff;
font-size: 2rem;
font-weight: 500;
left: 0;
transition: background-color 0.3s;
position: absolute;
width: 100%;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100%);
opacity: 0;
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
z-index: 1000;
&.error {
background-color: #d00820;
}
&.success {
.icon {
height: 1.8em;
margin-right: 0.5em;
margin-top: 0.1em;
width: 1.8em;
}
&.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
.content {
transition:
height 0.3s,
color 0.3s;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 16px;
border-radius: 4px;
background-color: #333;
color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
&.enter,
&.exitTo {
height: 0;
color: transparent;
.error {
.content {
background-color: #d32f2f;
}
}
.success {
.content {
background-color: #43a047;
}
}
.icon {
margin-right: 8px;
}

View File

@ -1,6 +1,5 @@
import { clsx } from 'clsx'
import { Show } from 'solid-js'
import { Transition } from 'solid-transition-group'
import { Show, createEffect } from 'solid-js'
import { useSnackbar } from '~/context/ui'
import { Icon } from '../_shared/Icon'
@ -10,29 +9,33 @@ import styles from './Snackbar.module.scss'
export const Snackbar = () => {
const { snackbarMessage } = useSnackbar()
let snackbarRef: HTMLDivElement | undefined
createEffect(() => {
if (snackbarMessage()?.body) {
snackbarRef?.classList.add(styles.show)
} else {
snackbarRef?.classList.remove(styles.show)
}
})
return (
<div
ref={snackbarRef}
class={clsx(styles.snackbar, {
[styles.error]: snackbarMessage()?.type === 'error',
[styles.success]: snackbarMessage()?.type === 'success'
})}
>
<ShowOnlyOnClient>
<Transition
enterClass={styles.enter}
exitToClass={styles.exitTo}
onExit={(_el, done) => setTimeout(() => done(), 300)}
>
<Show when={snackbarMessage()?.body}>
<div class={styles.content}>
<Show when={snackbarMessage()?.type === 'success'}>
<Icon name="check-success" class={styles.icon} />
</Show>
{snackbarMessage()?.body || ''}
</div>
</Show>
</Transition>
<Show when={snackbarMessage()?.body}>
<div class={styles.content}>
<Show when={snackbarMessage()?.type === 'success'}>
<Icon name="check-success" class={styles.icon} />
</Show>
{snackbarMessage()?.body || ''}
</div>
</Show>
</ShowOnlyOnClient>
</div>
)

View File

@ -1,5 +1,5 @@
import { JSX, Show, createSignal, onCleanup, onMount } from 'solid-js'
import { createTooltip } from '~/lib/createTooltip'
import { JSX, Show, createSignal, onMount } from 'solid-js'
import usePopper from 'solid-popper'
import styles from './Popover.module.scss'
@ -7,58 +7,63 @@ type Props = {
children: (setTooltipEl: (el: HTMLElement | null) => void) => JSX.Element
content: string | JSX.Element
disabled?: boolean
placement?: 'top' | 'bottom' | 'left' | 'right'
offset?: [number, number]
}
export const Popover = (props: Props) => {
const [show, setShow] = createSignal(false)
const [anchor, setAnchor] = createSignal<HTMLElement>()
const [tooltip, setTooltip] = createSignal<HTMLElement>()
const [popper, setPopper] = createSignal<HTMLElement>()
let tooltipInstance: ReturnType<typeof createTooltip> | undefined
usePopper(anchor, popper, {
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8]
}
},
{
name: 'flip',
options: {
fallbackPlacements: ['top', 'bottom']
}
}
]
})
onMount(() => {
const anchorEl = anchor()
const tooltipEl = tooltip()
const showEvents = ['mouseenter', 'focus']
const hideEvents = ['mouseleave', 'blur']
if (anchorEl && tooltipEl && !props.disabled) {
tooltipInstance = createTooltip(anchorEl, tooltipEl, {
placement: props.placement || 'top',
offset: props.offset || [0, 8]
const handleMouseOver = () => setShow(true)
const handleMouseOut = () => setShow(false)
if (!props.disabled) {
onMount(() => {
if (!anchor()) return
showEvents.forEach((event) => {
anchor()?.addEventListener(event, handleMouseOver)
})
}
})
onCleanup(() => {
tooltipInstance?.destroy()
})
const handleMouseOver = () => {
if (!props.disabled) {
setShow(true)
tooltipInstance?.update()
}
}
const handleMouseOut = () => {
setShow(false)
hideEvents.forEach((event) => {
anchor()?.addEventListener(event, handleMouseOut)
})
return () => {
showEvents.forEach((event) => {
anchor()?.removeEventListener(event, handleMouseOver)
})
hideEvents.forEach((event) => {
anchor()?.removeEventListener(event, handleMouseOut)
})
}
})
}
return (
<>
<div
onMouseEnter={handleMouseOver}
onMouseLeave={handleMouseOut}
onFocusIn={handleMouseOver}
onFocusOut={handleMouseOut}
>
{props.children(setAnchor)}
</div>
{props.children(setAnchor)}
<Show when={show() && !props.disabled}>
<div ref={setTooltip} class={styles.tooltip}>
<div ref={setPopper} class={styles.tooltip}>
{props.content}
<div class={styles.arrow} />
<div class={styles.arrow} data-popper-arrow={true} />
</div>
</Show>
</>

View File

@ -1,7 +1,10 @@
export function createTooltip(referenceElement?: Element, tooltipElement?: HTMLElement, options = {}) {
const defaultOptions = {
placement: 'top',
offset: [0, 8]
offset: [0, 8],
flip: {
fallbackPlacements: ['top', 'bottom']
}
}
const config = { ...defaultOptions, ...options }
@ -13,44 +16,60 @@ export function createTooltip(referenceElement?: Element, tooltipElement?: HTMLE
const offsetX = config.offset[0]
const offsetY = config.offset[1]
let placement = config.placement
let top = 0
let left = 0
switch (config.placement) {
case 'top': {
// Базовое позиционирование
switch (placement) {
case 'top':
top = rect.top - tooltipRect.height - offsetY
left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX
break
}
case 'bottom': {
case 'bottom':
top = rect.bottom + offsetY
left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX
break
}
case 'left': {
top = rect.top + (rect.height - tooltipRect.height) / 2 + offsetY
left = rect.left - tooltipRect.width - offsetX
break
}
case 'right': {
top = rect.top + (rect.height - tooltipRect.height) / 2 + offsetY
left = rect.right + offsetX
break
}
default: {
top = rect.top - tooltipRect.height - offsetY
left = rect.left + (rect.width - tooltipRect.width) / 2 + offsetX
}
// Добавьте case для 'left' и 'right', если необходимо
}
// Проверка на выход за границы окна и применение flip
if (top < 0 && config.flip.fallbackPlacements.includes('bottom')) {
top = rect.bottom + offsetY
placement = 'bottom'
} else if (top + tooltipRect.height > window.innerHeight && config.flip.fallbackPlacements.includes('top')) {
top = rect.top - tooltipRect.height - offsetY
placement = 'top'
}
// Применение позиции
tooltipElement.style.position = 'absolute'
tooltipElement.style.top = `${top}px`
tooltipElement.style.left = `${left}px`
tooltipElement.style.visibility = 'visible'
// Обновление класса для стрелки
tooltipElement.setAttribute('data-popper-placement', placement)
// Позиционирование стрелки
const arrow = tooltipElement.querySelector('[data-popper-arrow]') as HTMLElement
if (arrow) {
const arrowRect = arrow.getBoundingClientRect()
if (placement === 'top') {
arrow.style.bottom = '-4px'
arrow.style.left = `${tooltipRect.width / 2 - arrowRect.width / 2}px`
} else if (placement === 'bottom') {
arrow.style.top = '-4px'
arrow.style.left = `${tooltipRect.width / 2 - arrowRect.width / 2}px`
}
}
}
function showTooltip() {
if (tooltipElement) tooltipElement.style.visibility = 'visible'
updatePosition()
if (tooltipElement) {
tooltipElement.style.visibility = 'visible'
updatePosition()
}
}
function hideTooltip() {
@ -60,6 +79,7 @@ export function createTooltip(referenceElement?: Element, tooltipElement?: HTMLE
referenceElement?.addEventListener('mouseenter', showTooltip)
referenceElement?.addEventListener('mouseleave', hideTooltip)
window.addEventListener('resize', updatePosition)
window.addEventListener('scroll', updatePosition)
return {
update: updatePosition,
@ -67,6 +87,7 @@ export function createTooltip(referenceElement?: Element, tooltipElement?: HTMLE
referenceElement?.removeEventListener('mouseenter', showTooltip)
referenceElement?.removeEventListener('mouseleave', hideTooltip)
window.removeEventListener('resize', updatePosition)
window.removeEventListener('scroll', updatePosition)
}
}
}