popFloat
There are so many cases in user-interfaces where it's useful to pop-up a floating user interface element that a simple and reliable way of doing this seems like a good idea.
The problem with many such approaches is that they assume a highly specific use-case (e.g. popup menu or combo box) and while meeting the creator's intended purpose admirably, turn out to have some annoying limitation that prevents them handling the specific case at hand.
import { popFloat, positionFloat } from 'tosijs-ui'
import { button } from 'tosijs'.elements
const grid = preview.querySelector('.grid')
grid.addEventListener('click', (event) => {
const { target } = event
if (!target.closest('button')) {
return
}
const float = document.querySelector('.popped-float')
if (float === null) {
// create and position a float
popFloat({
class: 'popped-float',
content: [
'hello, I am a float',
button('close me', {
onClick(event){
event.target.closest('tosi-float').remove()
}
})
],
target,
position: target.dataset.float,
remainOnScroll: 'remove',
remainOnResize: 'remove'
})
} else {
// position an existing float
positionFloat(float, target, target.dataset.float, 'remove', 'remove')
}
})
<h2>Pop Float Demo</h2>
<div class="grid">
<button data-float="nw">nw</button>
<button data-float="n">n</button>
<button data-float="ne">ne</button>
<button data-float="wn">wn</button>
<span> </span>
<button data-float="en">en</button>
<button data-float="w">w</button>
<button data-float="auto">auto</button>
<button data-float="e">e</button>
<button data-float="ws">ws</button>
<button data-float="side">side</button>
<button data-float="es">es</button>
<button data-float="sw">sw</button>
<button data-float="s">s</button>
<button data-float="se">se</button>
</div>
.preview .grid {
display: grid;
grid-template-columns: 80px 80px 80px;
}
.popped-float {
display: flex;
flex-direction: column;
border-radius: 5px;
padding: 10px 15px;
background: var(--inset-bg);
box-shadow:
inset 0 0 0 1px var(--brand-color),
2px 10px 5px #0004;
}
popFloat
export interface PopFloatOptions {
class?: string
content: HTMLElement | ElementPart[]
target: HTMLElement
position?: FloatPosition
remainOnResize?: 'hide' | 'remove' | 'remain' // default is 'remove',
remainOnScroll?: 'hide' | 'remove' | 'remain' // default is 'remain',
draggable?: boolean
}
export const popFloat = (options: PopFloatOptions): TosiFloat
Create a <tosi-float> with the content provided, positioned as specified (or automatically).
positionFloat
export const positionFloat = (
element: HTMLElement,
target: HTMLElement,
position?: FloatPosition,
remainOnScroll: 'hide' | 'remove' | 'remain' = 'remain',
remainOnResize: 'hide' | 'remove' | 'remain' = 'remove',
draggable = false
): void
This allows you to, for example, provide a global menu that can be used on any element instead of needing to have a whole instance of the menu wrapped around every instance of the thing you want the menu to affect (a common anti-pattern of front-end frameworks).
Handling Overflow
positionFloat automatically sets two css-variables --max-height and --max-width on
the floating element to help you deal with overflow (e.g. in menus). E.g. if the float
is positioned with top: 125px then it will set --max-height: calc(100vh - 125px).
FloatPosition
export type FloatPosition =
| 'n'
| 's'
| 'e'
| 'w'
| 'ne'
| 'nw'
| 'se'
| 'sw'
| 'en'
| 'wn'
| 'es'
| 'ws'
| 'side'
| 'auto'
Draggable
Sometimes it's nice to have popup palettes and modeless dialogs the user can drag away.
popFloat() makes this really easy to do.
import { elements } from 'tosijs'
import { popFloat, icons } from 'tosijs-ui'
const { button, h4, p } = elements
preview.append(button(
'Draggable Popup',
{
class: 'spawn-draggable',
onClick(event) {
popFloat({
class: 'tearoff',
content: [
h4('Move me!'),
p('Iām delicious!'),
button(
icons.x(),
{
class: 'no-drag close-tearoff',
onClick(event) {
event.target.closest('tosi-float').remove()
}
}
)
],
target: event.target,
remainOnScroll: 'remain',
remainOnResize: 'remain',
draggable: true,
})
}
},
))
.tearoff {
--tearoff-bg: #fff6;
--tearoff-button-bg: #fff2;
--tearoff-color: #222;
--tearoff-hilite: #fff8;
--tearoff-shadow: #0002;
display: flex;
flex-direction: column;
border-radius: 20px;
padding: 10px 15px;
background: var(--tearoff-bg);
backdrop-filter: blur(6px);
box-shadow:
inset 1px 1px 0 1px var(--tearoff-hilite),
inset -1px -1px 0 1px var(--tearoff-shadow),
2px 5px 10px var(--tearoff-shadow);
width: 200px;
color: var(--tearoff-color);
--text-color: var(--tearoff-color);
}
.darkmode .tearoff {
--tearoff-bg: #0004;
--tearoff-button-bg: #0001;
--tearoff-color: #fff;
}
.tearoff > :first-child {
margin-top: 0;
}
.tearoff > :last-child {
margin-bottom: 0;
}
.spawn-draggable {
margin: 10px;
}
.close-tearoff {
position: absolute;
top: 4px;
right: 4px;
width: 32px;
height: 32px;
text-align: center;
padding: 0;
line-height: 32px;
background: var(--tearoff-button-bg);
border-radius: 100px;
box-shadow:
inset 1px 1px 0 1px var(--tearoff-hilite),
inset -1px -1px 0 1px var(--tearoff-shadow);
}