Skip to content

Commit 6069d6e

Browse files
committed
♻ Reworked Overflow Menu
1 parent 18254a3 commit 6069d6e

File tree

7 files changed

+220
-154
lines changed

7 files changed

+220
-154
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script type="module">
2+
import Component from "/js/overflow-button.js";
3+
const example = new Component({
4+
items: [
5+
{
6+
label: "Example",
7+
callback: () => {
8+
console.log("Hello word 1");
9+
},
10+
},
11+
{
12+
label: "Example",
13+
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" /></svg>`,
14+
callback: () => {
15+
console.log("Hello word 2");
16+
},
17+
},
18+
null,
19+
{
20+
danger: true,
21+
label: "Example",
22+
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>`,
23+
callback: () => {
24+
console.log("Hello word 3");
25+
},
26+
},
27+
],
28+
});
29+
document.body.appendChild(example);
30+
</script>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
overflow-button {
2+
position: relative;
3+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { html, render } from "lit-html";
2+
import SuperComponent from "@codewithkyle/supercomponent";
3+
import env from "~brixi/controllers/env";
4+
import { parseDataset } from "~brixi/utils/general";
5+
import OverflowMenu, { OverflowItem } from "~brixi/components/overflow-menu/overflow-menu";
6+
import { unsafeHTML } from "lit-html/directives/unsafe-html";
7+
import { UUID } from "@codewithkyle/uuid";
8+
import pos from "~brixi/controllers/pos";
9+
10+
type ButtonKind = "solid" | "outline" | "text";
11+
type ButtonColor = "primary" | "danger" | "grey" | "success" | "warning" | "info";
12+
type ButtonShape = "pill" | "round" | "sharp" | "default";
13+
type ButtonSize = "default" | "slim" | "large";
14+
15+
export interface IOverflowButton {
16+
icon: string;
17+
iconPosition: "left" | "right" | "center";
18+
kind: ButtonKind;
19+
color: ButtonColor;
20+
shape: ButtonShape;
21+
size: ButtonSize;
22+
tooltip: string;
23+
css: string;
24+
class: string;
25+
attributes: {
26+
[name: string]: string | number;
27+
};
28+
disabled: boolean;
29+
items: Array<OverflowItem>;
30+
}
31+
export interface OverflowButtonSettings {
32+
kind?: ButtonKind;
33+
color?: ButtonColor;
34+
shape?: ButtonShape;
35+
size?: ButtonSize;
36+
icon?: string;
37+
iconPosition?: "left" | "right" | "center";
38+
tooltip?: string;
39+
css?: string;
40+
class?: string;
41+
attributes?: {
42+
[name: string]: string | number;
43+
};
44+
disabled?: boolean;
45+
items: Array<OverflowItem>;
46+
}
47+
export default class OverflowButton extends SuperComponent<IOverflowButton> {
48+
private uid: string;
49+
50+
constructor(settings: OverflowButtonSettings) {
51+
super();
52+
this.uid = UUID();
53+
this.model = {
54+
kind: "text",
55+
color: "grey",
56+
shape: "round",
57+
size: "default",
58+
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" /></svg>`,
59+
iconPosition: "center",
60+
tooltip: "Open menu",
61+
css: "",
62+
class: "",
63+
attributes: {},
64+
disabled: false,
65+
items: [],
66+
};
67+
this.model = parseDataset<IOverflowButton>(this.dataset, this.model);
68+
env.css(["button"]).then(() => {
69+
this.set(settings);
70+
});
71+
}
72+
73+
override connected() {
74+
this.addEventListener("click", this.handleClick);
75+
}
76+
77+
private handleClick: EventListener = (e: Event) => {
78+
e.stopImmediatePropagation();
79+
const target = e.currentTarget as HTMLElement;
80+
const container = new OverflowMenu(this.uid, this.model.items);
81+
document.body.appendChild(container);
82+
pos.positionElementToElement(container, target);
83+
};
84+
85+
override render() {
86+
this.style.cssText = this.model.css;
87+
Object.keys(this.model.attributes).map((key) => {
88+
this.setAttribute(key, `${this.model.attributes[key]}`);
89+
});
90+
this.className = `bttn ${this.model.class}`;
91+
this.setAttribute("kind", this.model.kind);
92+
this.setAttribute("color", this.model.color);
93+
this.setAttribute("shape", this.model.shape);
94+
this.setAttribute("icon", this.model.iconPosition);
95+
this.setAttribute("size", this.model.size);
96+
if (this.model.disabled) {
97+
this.setAttribute("disabled", `${this.model.disabled}`);
98+
}
99+
this.setAttribute("tooltip", "");
100+
this.setAttribute("aria-label", this.model.tooltip);
101+
const view = html` ${unsafeHTML(this.model.icon)} `;
102+
render(view, this);
103+
}
104+
}
105+
env.bind("overflow-button", OverflowButton);

src/framework/components/overflow-menu/index.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script type="module">
22
import Example from "/js/overflow-menu.js";
3-
const test = new Example({
4-
items: [
3+
const test = new Example("test", [
54
{
65
label: "Example",
76
callback: () => {
@@ -25,6 +24,6 @@
2524
},
2625
},
2726
],
28-
});
27+
);
2928
document.body.appendChild(test);
3029
</script>

src/framework/components/overflow-menu/overflow-menu.scss

Lines changed: 2 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,4 @@
11
overflow-menu {
2-
display: inline-block;
3-
position: relative;
4-
5-
& > button {
6-
width: 36px;
7-
height: 36px;
8-
display: inline-flex;
9-
justify-content: center;
10-
align-items: center;
11-
transition: all 150ms var(--ease-in-out);
12-
user-select: none;
13-
cursor: pointer;
14-
color: var(--grey-400);
15-
outline-offset: 0;
16-
17-
&:focus-visible {
18-
outline: var(--focus-ring);
19-
outline-offset: var(--focus-ring-offset);
20-
transition: outline-offset 80ms var(--ease-in-out);
21-
}
22-
23-
&:hover,
24-
&:focus-visible {
25-
color: var(--grey-700);
26-
27-
&::before {
28-
opacity: 0.05;
29-
}
30-
}
31-
32-
&:active {
33-
color: var(--grey-700);
34-
outline-offset: 0;
35-
36-
&::before {
37-
opacity: 0.1;
38-
}
39-
}
40-
41-
&::before {
42-
content: "";
43-
display: inline-block;
44-
width: 100%;
45-
height: 100%;
46-
position: absolute;
47-
top: 0;
48-
left: 0;
49-
transition: all 80ms var(--ease-in-out);
50-
opacity: 0;
51-
background-color: var(--grey-500);
52-
border-radius: 50%;
53-
}
54-
55-
svg {
56-
width: 18px;
57-
height: 18px;
58-
}
59-
}
60-
}
61-
62-
overflow-menu-container {
63-
opacity: 0;
64-
visibility: hidden;
652
border-radius: 0.25rem;
663
background-color: var(--white);
674
border: 1px solid var(--grey-300);
@@ -70,7 +7,8 @@ overflow-menu-container {
707
display: inline-block;
718
position: fixed;
729
z-index: 1000;
73-
pointer-events: none;
10+
top: 0;
11+
left: 0;
7412

7513
&.is-visible {
7614
pointer-events: all;

src/framework/components/overflow-menu/overflow-menu.ts

Lines changed: 11 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import SuperComponent from "@codewithkyle/supercomponent";
22
import { html, render } from "lit-html";
33
import { unsafeHTML } from "lit-html/directives/unsafe-html";
44
import env from "~brixi/controllers/env";
5-
import { parseDataset } from "~brixi/utils/general";
6-
import { UUID as uuid } from "@codewithkyle/uuid";
75

86
export interface OverflowItem {
97
label: string;
@@ -12,41 +10,17 @@ export interface OverflowItem {
1210
danger?: boolean;
1311
}
1412
export interface IOverflowMenu {
15-
icon: string;
1613
items: Array<OverflowItem>;
17-
tooltip: string;
18-
class: string;
19-
css: string;
20-
attributes: {
21-
[name: string]: string | number;
22-
};
2314
uid: string;
2415
}
25-
export interface OverflowMenuSettings {
26-
items: Array<OverflowItem>;
27-
icon?: string;
28-
tooltip?: string;
29-
css?: string;
30-
class?: string;
31-
attributes?: {
32-
[name: string]: string | number;
33-
};
34-
}
3516
export default class OverflowMenu extends SuperComponent<IOverflowMenu> {
36-
constructor(settings: OverflowMenuSettings) {
17+
constructor(uid: string, items: Array<OverflowItem>) {
3718
super();
3819
this.model = {
39-
icon: `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" /></svg>`,
40-
items: [],
41-
tooltip: null,
42-
class: "",
43-
css: "",
44-
attributes: {},
45-
uid: uuid(),
20+
items: items,
21+
uid: uid,
4622
};
47-
this.model = parseDataset<IOverflowMenu>(this.dataset, this.model);
4823
env.css("overflow-menu").then(() => {
49-
this.set(settings, true);
5024
this.render();
5125
});
5226
}
@@ -55,27 +29,21 @@ export default class OverflowMenu extends SuperComponent<IOverflowMenu> {
5529
document.addEventListener(
5630
"click",
5731
() => {
58-
document.body.querySelectorAll(`overflow-menu-container[overflow-menu-container-id="${this.model.uid}"].is-visible`).forEach((container: HTMLElement) => {
59-
container.remove();
60-
});
32+
this.remove();
6133
},
6234
{ passive: true, capture: true }
6335
);
6436
window.addEventListener(
6537
"resize",
6638
() => {
67-
document.body.querySelectorAll(`overflow-container[overflow-menu-container-id="${this.model.uid}"].is-visible`).forEach((container: HTMLElement) => {
68-
container.remove();
69-
});
39+
this.remove();
7040
},
7141
{ passive: true, capture: true }
7242
);
7343
window.addEventListener(
7444
"scroll",
7545
() => {
76-
document.body.querySelectorAll(`overflow-container[overflow-menu-container-id="${this.model.uid}"].is-visible`).forEach((container: HTMLElement) => {
77-
container.remove();
78-
});
46+
this.remove();
7947
},
8048
{ passive: true, capture: true }
8149
);
@@ -84,54 +52,10 @@ export default class OverflowMenu extends SuperComponent<IOverflowMenu> {
8452
});
8553
}
8654

87-
private handleClick: EventListener = (e: Event) => {
88-
const target = e.currentTarget as HTMLElement;
89-
const bounds = target.getBoundingClientRect();
90-
const container = new OverflowMenuContainer(this.model.uid, this.model.items);
91-
document.body.appendChild(container);
92-
const containerBounds = container.getBoundingClientRect();
93-
let top = bounds.top + bounds.height;
94-
if (top + containerBounds.height >= window.innerHeight) {
95-
top = bounds.top - containerBounds.height;
96-
}
97-
let left = bounds.left;
98-
if (left + containerBounds.width >= window.innerWidth) {
99-
left = bounds.left + bounds.width - containerBounds.width;
100-
}
101-
container.style.top = `${top}px`;
102-
container.style.left = `${left}px`;
103-
container.classList.toggle("is-visible");
104-
};
105-
106-
override render() {
107-
this.style.cssText = this.model.css;
108-
this.className = this.model.class;
109-
Object.keys(this.model.attributes).map((key) => {
110-
this.setAttribute(key, `${this.model.attributes[key]}`);
111-
});
112-
const view = html`
113-
<button @click=${this.handleClick} sfx="button" type="button" aria-label="${this.model.tooltip || "open menu"}" tooltip>${unsafeHTML(this.model.icon)}</button>
114-
`;
115-
render(view, this);
116-
}
117-
}
118-
env.bind("overflow-menu", OverflowMenu);
119-
120-
class OverflowMenuContainer extends HTMLElement {
121-
private uid: string;
122-
private items: Array<OverflowItem>;
123-
124-
constructor(uid: string, items: Array<OverflowItem>) {
125-
super();
126-
this.uid = uid;
127-
this.items = items;
128-
this.render();
129-
}
130-
13155
private handleItemClick: EventListener = (e: Event) => {
13256
const target = e.currentTarget as HTMLElement;
13357
const index = parseInt(target.dataset.index);
134-
this.items?.[index]?.callback();
58+
this.model.items?.[index]?.callback();
13559
};
13660

13761
private renderItem(item: OverflowItem, index: number) {
@@ -146,14 +70,14 @@ class OverflowMenuContainer extends HTMLElement {
14670
`;
14771
}
14872

149-
private render() {
150-
this.setAttribute("overflow-menu-container-id", this.uid);
73+
override render() {
74+
this.setAttribute("overflow-menu-container-id", this.model.uid);
15175
const view = html`
152-
${this.items.map((item, index) => {
76+
${this.model.items.map((item, index) => {
15377
return this.renderItem(item, index);
15478
})}
15579
`;
15680
render(view, this);
15781
}
15882
}
159-
env.bind("overflow-menu-container", OverflowMenuContainer);
83+
env.bind("overflow-menu", OverflowMenu);

0 commit comments

Comments
 (0)