Skip to content

Commit 78afc0b

Browse files
feat(lxl-web): Redesign and swap primary action on suggestions (#1244)
* Temporarily remove load more button * Add utility classes * Add nowrap to locale selector * Redesign suggestions * Move wrapper component to supersearch folder * Remove persistent row * Remove redundant check * Use links instead of button for non-qualifiers * Re-add load more button * Add dropdown menu * Fix dangling divider * Force remount of dropdown menu * Open dropdown menus by clicking * Fix issue with missing resultItem * Fix prevention of arrow key cursor handling * Remove goto label * Remove badge * Update styling of action type label * Update styling of action type label * Add loadMoreLabel prop * Set add label to sr-only * Redesign action label yet again * Update styling again * Fix lowercase --------- Co-authored-by: Jesper Engström <[email protected]>
1 parent 850ea95 commit 78afc0b

File tree

13 files changed

+621
-289
lines changed

13 files changed

+621
-289
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte';
3+
import { computePosition, offset, shift, inline, flip, arrow } from '@floating-ui/dom';
4+
5+
export type MenuItem = { label: string; action: () => void };
6+
7+
type Props = {
8+
referenceElement: HTMLElement;
9+
onmouseover: (event: MouseEvent) => void;
10+
onmouseleave: (event: MouseEvent) => void;
11+
onfocus: (event: FocusEvent) => void;
12+
onblur: (event: FocusEvent) => void;
13+
menuItems?: MenuItem[];
14+
};
15+
16+
const { referenceElement, onmouseover, onmouseleave, onfocus, onblur, menuItems }: Props =
17+
$props();
18+
19+
let dropDownMenuElement: HTMLElement | undefined = $state();
20+
let arrowElement: HTMLDivElement | undefined = $state();
21+
22+
const arrowWidth = 14;
23+
const arrowHeight = 8;
24+
25+
function updatePosition() {
26+
computePosition(referenceElement, dropDownMenuElement, {
27+
middleware: [
28+
offset(8),
29+
shift({ padding: 24 }),
30+
arrow({ element: arrowElement, padding: 8 }),
31+
inline(),
32+
flip()
33+
]
34+
}).then(({ strategy, x, y, middlewareData, placement }) => {
35+
Object.assign(dropDownMenuElement.style, {
36+
position: strategy,
37+
left: `${x}px`,
38+
top: `${y}px`
39+
});
40+
if (middlewareData.arrow) {
41+
const { x: arrowX, y: arrowY } = middlewareData.arrow;
42+
43+
Object.assign(arrowElement.style, {
44+
top:
45+
(arrowY != null && `${arrowY}px`) ||
46+
(placement === 'bottom' && `-${arrowWidth}px`) ||
47+
'',
48+
left:
49+
(arrowX != null && `${arrowX}px`) ||
50+
(placement === 'right' && `-${arrowWidth}px`) ||
51+
'',
52+
right: (arrowX == null && placement === 'left' && `-${arrowWidth}px`) || '',
53+
transform:
54+
(placement === 'right' && 'rotate(90deg)') ||
55+
(placement === 'bottom' && 'rotate(180deg)') ||
56+
(placement === 'left' && 'rotate(270deg)') ||
57+
''
58+
});
59+
}
60+
});
61+
}
62+
63+
onMount(() => {
64+
updatePosition();
65+
});
66+
</script>
67+
68+
<!--
69+
@component
70+
Renders a drop down menu.
71+
Note that `DropDownMenu.svelte` isn't intended to be used directly in page templates – use the `use:dropDownMenu` instead (see `$lib/actions/dropDownMenu`).
72+
-->
73+
<div
74+
class="drop-down-menu"
75+
role="complementary"
76+
bind:this={dropDownMenuElement}
77+
{onmouseover}
78+
{onmouseleave}
79+
{onfocus}
80+
{onblur}
81+
>
82+
<nav class="menu-items">
83+
<ul>
84+
{#each menuItems as item}
85+
<li><button type="button" onclick={item.action}>{item.label}</button></li>
86+
{/each}
87+
</ul>
88+
</nav>
89+
<div class="arrow" bind:this={arrowElement}>
90+
<svg
91+
aria-hidden="true"
92+
width={arrowWidth}
93+
height={arrowWidth}
94+
viewBox="0 0 {arrowWidth} {arrowWidth}"
95+
>
96+
<path d="M0 0L{arrowWidth / 2} {arrowHeight}L{arrowWidth} 0" fill="#fff" stroke="#c3c3c3" />
97+
</svg>
98+
</div>
99+
</div>
100+
101+
<style lang="postcss">
102+
.drop-down-menu {
103+
position: absolute;
104+
top: 0;
105+
left: 0;
106+
z-index: 100;
107+
width: max-content;
108+
max-width: theme(maxWidth.sm);
109+
border: 1px solid rgb(var(--color-primary) / 0.32);
110+
border-radius: theme(borderRadius.sm);
111+
background: theme(backgroundColor.cards);
112+
font-size: theme(fontSize.sm);
113+
box-shadow: theme(boxShadow.xl);
114+
}
115+
116+
.arrow {
117+
position: absolute;
118+
}
119+
120+
.menu-items button {
121+
display: flex;
122+
align-items: center;
123+
text-align: left;
124+
padding: 0 theme(padding.4);
125+
width: 100%;
126+
min-height: 44px;
127+
cursor: pointer;
128+
129+
&:hover {
130+
background: theme(backgroundColor.main);
131+
}
132+
}
133+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { Action } from 'svelte/action';
2+
import DropDownMenu, { type MenuItem } from './DropDownMenu.svelte';
3+
import { mount, unmount } from 'svelte';
4+
5+
type Parameter = {
6+
placeAsSibling?: boolean; // place DropDownMenu next to node in the DOM (to force it on top of modal, for example)
7+
menuItems: MenuItem[];
8+
};
9+
10+
export const dropdownMenu: Action<HTMLElement, Parameter> = (
11+
node,
12+
{ placeAsSibling, menuItems }
13+
) => {
14+
const ATTACH_DELAY = 500;
15+
const REMOVE_DELAY = 200;
16+
17+
$effect(() => {
18+
// setup goes here
19+
let container = document.getElementById('floating-elements-container') || document.body; // See https://atfzl.com/articles/don-t-attach-tooltips-to-document-body
20+
21+
if (placeAsSibling && node.parentElement) {
22+
container = node.parentElement;
23+
}
24+
25+
let attached = false;
26+
let floatingElement: DropDownMenu | null;
27+
let cancelAttach: ((reason?: unknown) => void) | undefined;
28+
let cancelRemove: ((reason?: unknown) => void) | undefined;
29+
30+
node.addEventListener('click', attachDropDownMenu);
31+
node.addEventListener('mouseover', attachDropDownMenu);
32+
node.addEventListener('mouseout', removeDropDownMenu);
33+
node.addEventListener('focus', attachDropDownMenu);
34+
node.addEventListener('blur', removeDropDownMenu);
35+
36+
async function attachDropDownMenu(event: MouseEvent | FocusEvent) {
37+
try {
38+
cancelAttach?.(); // cancel earlier promises to ensure DropDownMenus doesn't appear after navigating
39+
cancelRemove?.();
40+
if (!attached) {
41+
await new Promise((resolve, reject) => {
42+
cancelAttach = reject; // allows promise rejection from outside the promise constructor scope
43+
if (event.type === 'click') {
44+
resolve(undefined);
45+
} else {
46+
setTimeout(resolve, ATTACH_DELAY);
47+
}
48+
});
49+
50+
floatingElement = mount(DropDownMenu, {
51+
target: container,
52+
props: {
53+
referenceElement: node,
54+
onmouseover: startFloatingElementInteraction,
55+
onfocus: startFloatingElementInteraction,
56+
onmouseleave: endFloatingElementInteraction,
57+
onblur: endFloatingElementInteraction,
58+
menuItems
59+
}
60+
});
61+
62+
attached = true;
63+
}
64+
// eslint-disable-next-line no-empty
65+
} catch {}
66+
}
67+
68+
async function removeDropDownMenu() {
69+
try {
70+
cancelAttach?.();
71+
await new Promise((resolve, reject) => {
72+
cancelRemove = reject;
73+
setTimeout(resolve, REMOVE_DELAY);
74+
});
75+
destroyDropDownMenu();
76+
// eslint-disable-next-line no-empty
77+
} catch {}
78+
}
79+
80+
function destroyDropDownMenu() {
81+
cancelAttach?.();
82+
cancelRemove?.();
83+
if (attached && floatingElement) {
84+
unmount(floatingElement);
85+
}
86+
attached = false;
87+
}
88+
89+
function startFloatingElementInteraction() {
90+
cancelRemove?.();
91+
}
92+
93+
function endFloatingElementInteraction() {
94+
removeDropDownMenu();
95+
}
96+
97+
return () => {
98+
destroyDropDownMenu();
99+
node.removeEventListener('click', attachDropDownMenu);
100+
node.removeEventListener('mouseover', attachDropDownMenu);
101+
node.removeEventListener('mouseout', removeDropDownMenu);
102+
node.removeEventListener('focus', attachDropDownMenu);
103+
node.removeEventListener('blur', removeDropDownMenu);
104+
};
105+
});
106+
};
107+
108+
export default dropdownMenu;

0 commit comments

Comments
 (0)