Add popover util (#96)

* Add popover component

* [WIP] Popover component

* update ref

* conversation resolve

* conversation resolve (conditional render icon)

* conversation resolve (onMount instead createEffect)
This commit is contained in:
Ilya Y 2023-05-13 20:00:58 +03:00 committed by GitHub
parent cbb254a907
commit 4d147e3eb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 226 additions and 39 deletions

35
package-lock.json generated
View File

@ -18,6 +18,7 @@
"i18next": "22.4.15",
"mailgun.js": "8.2.1",
"node-fetch": "3.3.1",
"solid-popper": "0.3.0",
"typograf": "7.1.0"
},
"devDependencies": {
@ -5541,7 +5542,6 @@
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@ -8389,8 +8389,7 @@
"node_modules/csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
"dev": true
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@ -18106,7 +18105,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz",
"integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==",
"dev": true,
"engines": {
"node": ">=10"
}
@ -18305,12 +18303,23 @@
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.5.tgz",
"integrity": "sha512-GfJ8na1e9FG1oAF5xC24BM+ATLym0sfH+ZblkbBFpueYdq3fWAoA5Ve+jGeIeLI7jmMGfa0rUaKruszNm2sH8w==",
"dev": true,
"dependencies": {
"csstype": "^3.1.0",
"seroval": "^0.5.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==",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@popperjs/core": "^2.11",
"solid-js": "^1.2"
}
},
"node_modules/solid-refresh": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.5.1.tgz",
@ -24661,8 +24670,7 @@
"@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"dev": true
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw=="
},
"@remirror/core-constants": {
"version": "2.0.0",
@ -26768,8 +26776,7 @@
"csstype": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==",
"dev": true
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
},
"damerau-levenshtein": {
"version": "1.0.8",
@ -33967,8 +33974,7 @@
"seroval": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.5.1.tgz",
"integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g==",
"dev": true
"integrity": "sha512-ZfhQVB59hmIauJG5Ydynupy8KHyr5imGNtdDhbZG68Ufh1Ynkv9KOYOAABf71oVbQxJ8VkWnMHAjEHE7fWkH5g=="
},
"set-blocking": {
"version": "2.0.0",
@ -34107,12 +34113,17 @@
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.7.5.tgz",
"integrity": "sha512-GfJ8na1e9FG1oAF5xC24BM+ATLym0sfH+ZblkbBFpueYdq3fWAoA5Ve+jGeIeLI7jmMGfa0rUaKruszNm2sH8w==",
"dev": true,
"requires": {
"csstype": "^3.1.0",
"seroval": "^0.5.0"
}
},
"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==",
"requires": {}
},
"solid-refresh": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.5.1.tgz",

View File

@ -38,6 +38,7 @@
"i18next": "22.4.15",
"mailgun.js": "8.2.1",
"node-fetch": "3.3.1",
"solid-popper": "0.3.0",
"typograf": "7.1.0"
},
"devDependencies": {

View File

@ -1,9 +1,8 @@
import styles from './Header.module.scss'
import { clsx } from 'clsx'
import { router, useRouter } from '../../stores/router'
import { Icon } from '../_shared/Icon'
import { createMemo, createSignal, Show } from 'solid-js'
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import Notifications from './Notifications'
import { ProfilePopup } from './ProfilePopup'
import Userpic from '../Author/Userpic'
@ -14,11 +13,18 @@ import { useLocalize } from '../../context/localize'
import { getPagePath } from '@nanostores/router'
import { Button } from '../_shared/Button'
import { useEditorContext } from '../../context/editor'
import { Popover } from '../_shared/Popover'
type HeaderAuthProps = {
setIsProfilePopupVisible: (value: boolean) => void
}
type IconedButton = {
value: string
icon: string
action: () => void
}
const MD_WIDTH_BREAKPOINT = 992
export const HeaderAuth = (props: HeaderAuthProps) => {
const { t } = useLocalize()
const { page } = useRouter()
@ -64,6 +70,40 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
publishShout()
}
const [width, setWidth] = createSignal(0)
const handleResize = () => setWidth(window.innerWidth)
onMount(() => {
handleResize()
window.addEventListener('resize', handleResize)
onCleanup(() => window.removeEventListener('resize', handleResize))
})
const renderIconedButton = (iconedButtonProps: IconedButton) => {
return (
<Show
when={width() < MD_WIDTH_BREAKPOINT}
fallback={
<Button
value={<span class={styles.textLabel}>{iconedButtonProps.value}</span>}
variant={'outline'}
onClick={handleSaveButtonClick}
/>
}
>
<Popover content={iconedButtonProps.value}>
{(ref) => (
<Button
ref={ref}
variant={'outline'}
onClick={handleSaveButtonClick}
value={<Icon name={iconedButtonProps.icon} class={styles.icon} />}
/>
)}
</Popover>
</Show>
)
}
return (
<ShowOnlyOnClient>
<Show when={isSessionLoaded()} keyed={true}>
@ -90,37 +130,32 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
<Show when={showSaveButton()}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<Button
value={
<>
<span class={styles.textLabel}>{t('Save')}</span>
<Icon name="save" class={styles.icon} />
</>
}
variant={'outline'}
onClick={handleSaveButtonClick}
/>
{renderIconedButton({
value: t('Save'),
icon: 'save',
action: handleSaveButtonClick
})}
</div>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<Button
value={
<>
<span class={styles.textLabel}>{t('Publish')}</span>
<Icon name="publish" class={styles.icon} />
</>
}
variant={'outline'}
onClick={handlePublishButtonClick}
/>
{renderIconedButton({
value: t('Publish'),
icon: 'publish',
action: handlePublishButtonClick
})}
</div>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
<Button
value={<Icon name="burger" />}
variant={'outline'}
onClick={handleBurgerButtonClick}
/>
<Popover content={t('Settings')}>
{(ref) => (
<Button
ref={ref}
value={<Icon name="burger" />}
variant={'outline'}
onClick={handleBurgerButtonClick}
/>
)}
</Popover>
</div>
</Show>

View File

@ -11,11 +11,19 @@ type Props = {
disabled?: boolean
onClick?: () => void
class?: string
ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void)
}
export const Button = (props: Props) => {
return (
<button
ref={(el) => {
if (typeof props.ref === 'function') {
props.ref(el)
return
}
props.ref = el
}}
onClick={props.onClick}
type={props.type ?? 'button'}
disabled={props.loading || props.disabled}

View File

@ -0,0 +1,43 @@
.tooltip {
padding: 8px;
background: #141414;
font-size: 1.2rem;
color: #fff;
border-radius: 4px;
position: relative;
z-index: 100;
.arrow,
.arrow::before {
position: absolute;
width: 8px;
height: 8px;
background: inherit;
}
.arrow {
visibility: hidden;
}
.arrow::before {
visibility: visible;
content: '';
transform: rotate(45deg);
}
&[data-popper-placement^='top'] > .arrow {
bottom: -4px;
}
&[data-popper-placement^='bottom'] > .arrow {
top: -4px;
}
&[data-popper-placement^='left'] > .arrow {
right: -4px;
}
&[data-popper-placement^='right'] > .arrow {
left: -4px;
}
}

View File

@ -0,0 +1,66 @@
import { createEffect, createSignal, JSX, JSXElement, onMount, Show } from 'solid-js'
import usePopper from 'solid-popper'
import styles from './Popover.module.scss'
type Props = {
children: (setTooltipEl: (el: HTMLElement | null) => void) => JSX.Element
content: string
}
export const Popover = (props: Props) => {
const [show, setShow] = createSignal(false)
const [anchor, setAnchor] = createSignal<HTMLElement>()
const [popper, setPopper] = createSignal<HTMLElement>()
usePopper(anchor, popper, {
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8]
}
},
{
name: 'flip',
options: {
fallbackPlacements: ['top', 'bottom']
}
}
]
})
const showEvents = ['mouseenter', 'focus']
const hideEvents = ['mouseleave', 'blur']
const handleMouseOver = () => setShow(true)
const handleMouseOut = () => setShow(false)
onMount(() => {
showEvents.forEach((event) => {
anchor().addEventListener(event, handleMouseOver)
})
hideEvents.forEach((event) => {
anchor().addEventListener(event, handleMouseOut)
})
return () => {
showEvents.forEach((event) => {
anchor().removeEventListener(event, handleMouseOver)
})
hideEvents.forEach((event) => {
anchor().removeEventListener(event, handleMouseOut)
})
}
})
return (
<>
{props.children(setAnchor)}
<Show when={show()}>
<div ref={setPopper} class={styles.tooltip} role="tooltip">
{props.content}
<div class={styles.arrow} data-popper-arrow={true} />
</div>
</Show>
</>
)
}

View File

@ -0,0 +1,22 @@
## Usage Example
```JS
import Popover from './Popover';
<Popover content={'This is popover text'}>
{(triggerRef: (el) => void) => (
<Button value="Hover me" ref={triggerRef} />
)}
</Popover>
```
### or
```JS
import Popover from './Popover';
<Popover content={'This is popover text'}>
{(triggerRef: (el) => void) => (
<div ref={triggerRef}>Hover me</div>
)}
</Popover>
```

View File

@ -0,0 +1 @@
export { Popover } from './Popover'