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

View File

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

View File

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

View File

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