Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve popup behavior #986

Merged
merged 2 commits into from
Feb 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/docs/layouts/DocsThemer/DocsThemer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,8 @@
{#each inputSettings.colorProps as c}<option value={c.value}>{c.label}</option>{/each}
</select>
<div
use:popup={Object.assign(tooltipSettings, {
target: 'popup-' + i
})}
class="badge-icon aspect-square relative -top-1 right-4 z-10 hover:scale-125 transition-all"
use:popup={{ ...tooltipSettings, ...{ target: 'popup-' + i } }}
class="badge-icon aspect-square relative -top-2 right-4 z-[1] hover:scale-125 transition-all"
class:!text-stone-900={contrastReport.fails}
class:!bg-red-500={contrastReport.fails}
class:!text-zinc-900={contrastReport.largeAA}
Expand All @@ -164,7 +162,7 @@
</div>
<div
data-popup={'popup-' + i}
class=" text-xs card variant-filled p-2 max-w-xs"
class="text-xs card variant-filled p-2 whitespace-nowrap"
class:!variant-filled-red-500={contrastReport.fails}
class:!variant-filled-amber-500={contrastReport.largeAA}
class:!variant-filled-green-500={contrastReport.smallAAA || contrastReport.smallAA}
Expand Down
59 changes: 38 additions & 21 deletions src/lib/utilities/Popup/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ export const storePopup: Writable<any> = writable(undefined);

// Action
export function popup(node: HTMLElement, args: PopupSettings) {
const { event = 'click', target, placement, middleware, state }: PopupSettings = args;
// prettier-ignore
const {
event = 'click',
target,
placement,
closeQuery = 'a[href], button',
middleware,
state
}: PopupSettings = args;
if (!event || !target) return;

// Local
Expand All @@ -26,9 +34,6 @@ export function popup(node: HTMLElement, args: PopupSettings) {
function render(): void {
if (!elemPopup || !computePosition) return;

// Set Focusable State
setFocusableState();

// Construct Middlware
// Note the order: https://floating-ui.com/docs/middleware#ordering
const genMiddlware = [];
Expand Down Expand Up @@ -70,6 +75,9 @@ export function popup(node: HTMLElement, args: PopupSettings) {
});
}
});

// Set Focusable State
setFocusableState();
}

// Set Focusable State
Expand All @@ -85,19 +93,27 @@ export function popup(node: HTMLElement, args: PopupSettings) {
focusableElems[0]?.focus();
}

// Click Handlers
const onNodeClick = () => {
show();
};
// Window Click Handler
const onWindowClick = (event: any) => {
// Avoid race condition with onNodeClick()
setTimeout(() => {
const outsideNode = node && !node.contains(event.target);
const outsideMenu = elemPopup && !elemPopup.contains(event.target);
if (outsideNode && outsideMenu) {
hide();
if (!node || !elemPopup) return;
// If click is within the trigger node
const clickTriggerNode = node.contains(event.target);
if (clickTriggerNode) {
isVisible == false ? show() : close();
} else {
// If click is outside the popup
const clickedOutsidePopup = elemPopup && !elemPopup.contains(event.target);
if (clickedOutsidePopup) {
close();
} else {
// If click is interactive child element within popup (ex: anchor or button)
const interactiveMenuElems = elemPopup?.querySelectorAll(closeQuery);
if (!interactiveMenuElems.length) return;
interactiveMenuElems.forEach((elem) => {
if (elem.contains(event.target)) close();
});
}
}, 1);
}
};

// Hover Handlers
Expand All @@ -107,21 +123,22 @@ export function popup(node: HTMLElement, args: PopupSettings) {
stateEventHandler(true);
};
const onMouseOut = () => {
hide();
close();
isVisible = false;
stateEventHandler(false);
};

// Visbility
function show(): void {
if (!elemPopup) return;
render(); // update
elemPopup.style.display = 'block';
elemPopup.style.opacity = '1';
elemPopup.style.pointerEvents = 'initial';
isVisible = true;
stateEventHandler(true);
}
function hide(): void {
function close(): void {
if (!elemPopup) return;
elemPopup.style.opacity = '0';
const cssTransitionDuration = parseFloat(window.getComputedStyle(elemPopup).transitionDuration.replace('s', '')) * 1000;
Expand All @@ -146,7 +163,7 @@ export function popup(node: HTMLElement, args: PopupSettings) {
// TODO: || (document.activeElement !== node && key === 'Tab')
if (key === 'Escape') {
event.preventDefault();
hide();
close();
node.focus();
return;
} else if (key === 'ArrowDown') {
Expand Down Expand Up @@ -180,11 +197,11 @@ export function popup(node: HTMLElement, args: PopupSettings) {
// Event Listners
if (event === 'click') {
window.addEventListener('click', onWindowClick, true);
node.addEventListener('click', onNodeClick, true);
// node.addEventListener('click', onNodeClick, true);
}
if (event === 'hover') {
node.addEventListener('mouseover', show, true);
node.addEventListener('mouseout', hide, true);
node.addEventListener('mouseout', close, true);
}
if (event === 'hover-click') {
node.addEventListener('mouseover', show, true);
Expand All @@ -200,7 +217,7 @@ export function popup(node: HTMLElement, args: PopupSettings) {
},
destroy() {
window.removeEventListener('click', onWindowClick, true);
node.removeEventListener('click', onNodeClick, true);
// node.removeEventListener('click', onNodeClick, true);
node.removeEventListener('mouseover', onMouseOver, true);
node.removeEventListener('mouseout', onMouseOut, true);
// ---
Expand Down
10 changes: 6 additions & 4 deletions src/lib/utilities/Popup/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ type Direction = 'top' | 'bottom' | 'left' | 'right';
/** Placement https://floating-ui.com/docs/computePosition#placement */
type Placement = Direction | `${Direction}-start` | `${Direction}-end`;

// Middleware
interface Middleware {
// Options & Middlware
interface Middlware {
/** Offset options: https://floating-ui.com/docs/offset */
offset?: number | Record<string, any>;
/** Shift options: https://floating-ui.com/docs/shift */
Expand All @@ -25,8 +25,10 @@ export interface PopupSettings {
target: string;
/** Set the placement position. Defaults 'bottom'. */
placement?: Placement;
/** Provide options for each middleware. */
middleware?: Middleware;
/** Query list of elements that will close the popup. Default: `'a[href], button'`. */
closeQuery?: string;
/** Provide additional options and middleware settings. */
middleware?: Middlware;
/** Provide an optional callback function to monitor open/close state. */
state?: (event: { state: boolean }) => void;
}
35 changes: 27 additions & 8 deletions src/routes/(inner)/utilities/popups/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
['<code>event</code>', 'string', 'click', 'click | hover | hover-click', 'Provide the event type'],
['<code>target</code>', 'string', '-', '-', 'Match the popup data value of <code>[data-popup]</code>'],
['<code>placement</code>', 'string', '-', 'bottom', 'Set the placement position.'],
['<code>middleware</code>', 'object', '-', '-', 'Provide options for each middleware.'],
['<code>options</code>', 'object', '-', '-', 'Provide options and middleware settings.'],
['<code>state</code>', 'function', '-', '-', 'Provide an optional callback function to monitor open/close state.']
],
keyboard: [
Expand Down Expand Up @@ -58,7 +58,8 @@
let exampleCombobox: PopupSettings = {
event: 'click',
target: 'exampleCombobox',
placement: 'bottom'
placement: 'bottom',
closeQuery: '.listbox-item'
// state: (e: any) => console.log('tooltip', e)
};
let listboxValue: string;
Expand All @@ -74,8 +75,9 @@
<span>Tooltip</span>
<span class="badge bg-white/10 dark:bg-black/10">Hover</span>
</button>
<div class="card variant-filled-primary p-4 w-72 shadow-xl" data-popup="exampleTooltip">
This is a tooltip.
<div class="text-xs text-center card variant-filled-primary p-2 whitespace-nowrap shadow-xl" data-popup="exampleTooltip">
This is a <strong>tooltip</strong> example.
<!-- Arrow -->
<div class="arrow variant-filled-primary" />
</div>
</div>
Expand All @@ -85,11 +87,14 @@
<span>Menu</span>
<span class="badge bg-white/10 dark:bg-black/10">Tap</span>
</button>
<div class="card variant-filled-secondary p-4 w-72 shadow-xl" data-popup="exampleMenu">
<div class="card variant-filled-secondary p-4 w-72 shadow-xl space-y-4" data-popup="exampleMenu">
<p class="font-bold">This is a <strong>Menu</strong> example.</p>
<p>
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Dicta amet nam hic aspernatur cum porro praesentium. Voluptates velit
ex ad eius sit! Sit deserunt ex accusamus quod fugit enim in?
</p>
<button class="btn variant-filled w-full">Close Me</button>
<!-- Arrow -->
<div class="arrow variant-filled-secondary" />
</div>
</div>
Expand Down Expand Up @@ -193,21 +198,35 @@ let exampleSettings: PopupSettings = {
};
`}
/>
<!-- Middleware -->
<!-- Close Query -->
<h3>Close Query</h3>
<!-- prettier-ignore -->
<p>When using click events, this setting uses <em>querySelectorAll</em> to select all elements within the popup that will trigger a close event. By default this is set to <code>'a[href], button'</code>, which means clicking anchor or buttons within the popup will cause it to close.</p>
<CodeBlock
language="ts"
code={`
let exampleSettings: PopupSettings = {
// Limit to listbox items only:
closeQuery: '.listbox-item',
};
`}
/>
<!-- Middlware -->
<div class="flex items-center space-x-2">
<h3>Middleware</h3>
<h3>Middlware</h3>
<span class="badge variant-filled-error">Advanced</span>
</div>
<!-- prettier-ignore -->
<p>
You can modify settings for select <a href="https://floating-ui.com/docs/middleware" target="_blank" rel="noreferrer">Floating UI middleware</a> within <code>PopupSettings</code>. These are passed verbatim.
You can provide <a href="https://floating-ui.com/docs/middleware" target="_blank" rel="noreferrer">Floating UI middleware</a> settings within <code>PopupSettings</code>. These settings are passed verbatim.
</p>
<CodeBlock
language="ts"
code={`
let exampleSettings: PopupSettings = {
// ...
middleware: {
// Floating UI Middlware
/** https://floating-ui.com/docs/offset */
offset: 24, // or { ... }
/** https://floating-ui.com/docs/shift */
Expand Down