Skip to content

Commit b3fc26a

Browse files
committed
Refactor emoji picker to use shared floating panel
1 parent a6934b0 commit b3fc26a

2 files changed

Lines changed: 120 additions & 74 deletions

File tree

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.1.4/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",

packages/comment-widget/src/emoji-button.ts

Lines changed: 119 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import './icons/icon-loading';
2-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
3-
//@ts-ignore
42
import en from '@emoji-mart/data/i18n/en.json';
5-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6-
//@ts-ignore
73
import es from '@emoji-mart/data/i18n/es.json';
8-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
9-
//@ts-ignore
104
import zh from '@emoji-mart/data/i18n/zh.json';
115
import {
126
autoUpdate,
@@ -31,18 +25,79 @@ const localeMap = {
3125
es: es,
3226
};
3327

28+
const sharedEmojiPanel = {
29+
wrapper: null as HTMLDivElement | null,
30+
picker: null as Picker | null,
31+
activeButton: null as EmojiButton | null,
32+
33+
init() {
34+
if (this.wrapper) return this.wrapper;
35+
36+
const styleElement = document.createElement('style');
37+
styleElement.textContent = `
38+
.form__emoji-panel {
39+
box-shadow: 0 0.5em 1em rgba(0, 0, 0, 0.1);
40+
border-radius: 0.875em;
41+
overflow: hidden;
42+
display: none;
43+
}
44+
45+
.form__emoji-panel.visible {
46+
display: block;
47+
animation: fadeInUp 0.3s both;
48+
}
49+
50+
@keyframes fadeInUp {
51+
from {
52+
opacity: 0;
53+
transform: translate3d(0, 5%, 0);
54+
}
55+
to {
56+
opacity: 1;
57+
transform: translate3d(0, 0, 0);
58+
}
59+
}
60+
61+
em-emoji-picker {
62+
--rgb-color: var(--halo-cw-emoji-picker-rgb-color, 34, 36, 39);
63+
--rgb-accent: var(--halo-cw-emoji-picker-rgb-accent, 34, 102, 237);
64+
--rgb-background: var(--halo-cw-emoji-picker-rgb-background, 255, 255, 255);
65+
--rgb-input: var(--halo-cw-emoji-picker-rgb-input, 255, 255, 255);
66+
--color-border: var(--halo-cw-emoji-picker-color-border, rgba(0, 0, 0, .05));
67+
--color-border-over: var(--halo-cw-emoji-picker-color-border-over, rgba(0, 0, 0, .1));
68+
}
69+
`;
70+
document.head.appendChild(styleElement);
71+
72+
this.wrapper = document.createElement('div');
73+
this.wrapper.className = 'form__emoji-panel';
74+
this.wrapper.style.position = 'absolute';
75+
this.wrapper.style.zIndex = '9999';
76+
document.body.appendChild(this.wrapper);
77+
78+
return this.wrapper;
79+
},
80+
81+
show() {
82+
if (this.wrapper) {
83+
this.wrapper.classList.add('visible');
84+
}
85+
},
86+
87+
hide() {
88+
if (this.wrapper) {
89+
this.wrapper.classList.remove('visible');
90+
}
91+
},
92+
};
93+
3494
export class EmojiButton extends LitElement {
3595
@state()
3696
emojiPickerVisible = false;
3797

3898
@state()
3999
emojiLoading = false;
40100

41-
@state()
42-
emojiPicker: Picker | null = null;
43-
44-
emojiPickerWrapperRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
45-
46101
buttonRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
47102

48103
cleanupAutoUpdate: (() => void) | null = null;
@@ -58,11 +113,16 @@ export class EmojiButton extends LitElement {
58113
override connectedCallback() {
59114
super.connectedCallback();
60115
document.addEventListener('click', this.handleClickOutside, true);
116+
sharedEmojiPanel.init();
61117
}
62118

63119
override disconnectedCallback() {
64120
document.removeEventListener('click', this.handleClickOutside, true);
65121
this.cleanupFloating();
122+
if (sharedEmojiPanel.activeButton === this) {
123+
sharedEmojiPanel.activeButton = null;
124+
sharedEmojiPanel.hide();
125+
}
66126
super.disconnectedCallback();
67127
}
68128

@@ -74,8 +134,22 @@ export class EmojiButton extends LitElement {
74134
}
75135

76136
handleClickOutside(event: Event) {
77-
if (this.emojiPickerVisible && !event.composedPath().includes(this)) {
78-
this.emojiPickerVisible = false;
137+
if (
138+
this.emojiPickerVisible &&
139+
!event.composedPath().includes(this) &&
140+
sharedEmojiPanel.wrapper &&
141+
!event.composedPath().includes(sharedEmojiPanel.wrapper)
142+
) {
143+
this.closeEmojiPicker();
144+
}
145+
}
146+
147+
closeEmojiPicker() {
148+
this.emojiPickerVisible = false;
149+
this.cleanupFloating();
150+
sharedEmojiPanel.hide();
151+
if (sharedEmojiPanel.activeButton === this) {
152+
sharedEmojiPanel.activeButton = null;
79153
}
80154
}
81155

@@ -84,58 +158,65 @@ export class EmojiButton extends LitElement {
84158
e.preventDefault();
85159

86160
if (this.emojiPickerVisible) {
87-
this.emojiPickerVisible = false;
88-
this.cleanupFloating();
161+
this.closeEmojiPicker();
89162
return;
90163
}
91164

92-
if (this.emojiPickerWrapperRef.value?.children.length) {
165+
if (
166+
sharedEmojiPanel.activeButton &&
167+
sharedEmojiPanel.activeButton !== this
168+
) {
169+
sharedEmojiPanel.activeButton.closeEmojiPicker();
170+
}
171+
172+
sharedEmojiPanel.activeButton = this;
173+
174+
if (sharedEmojiPanel.wrapper?.children.length) {
93175
this.emojiPickerVisible = true;
176+
sharedEmojiPanel.show();
94177
this.setupFloating();
95178
return;
96179
}
97180

98181
this.emojiLoading = true;
99182

100-
const { Picker } = await import(
101-
/* webpackChunkName: "emoji-mart" */ 'emoji-mart'
102-
);
183+
const { Picker } = await import('emoji-mart');
184+
const { default: data } = await import('@emoji-mart/data');
103185

104-
const { default: data } = await import(
105-
/* webpackChunkName: "emoji-mart-data" */ '@emoji-mart/data'
106-
);
107-
108-
const emojiPicker = new Picker({
186+
sharedEmojiPanel.picker = new Picker({
109187
data,
110188
onEmojiSelect: ({ native }: { native: string }) => {
111-
this.dispatchEvent(
112-
new CustomEvent('emoji-select', { detail: { native } })
113-
);
189+
const activeButton = sharedEmojiPanel.activeButton;
190+
if (activeButton) {
191+
activeButton.dispatchEvent(
192+
new CustomEvent('emoji-select', { detail: { native } })
193+
);
194+
activeButton.closeEmojiPicker();
195+
}
114196
},
115197
i18n: localeMap[getLocale()],
116198
});
117199

118-
this.emojiPickerWrapperRef.value?.appendChild(
119-
emojiPicker as unknown as Node
120-
);
200+
if (sharedEmojiPanel.wrapper) {
201+
sharedEmojiPanel.wrapper.appendChild(
202+
sharedEmojiPanel.picker as unknown as Node
203+
);
204+
sharedEmojiPanel.show();
205+
}
121206

122207
this.emojiPickerVisible = true;
123208
this.emojiLoading = false;
124209
this.setupFloating();
125210
}
126211

127212
setupFloating() {
128-
if (!this.buttonRef.value || !this.emojiPickerWrapperRef.value) return;
213+
if (!this.buttonRef.value || !sharedEmojiPanel.wrapper) return;
129214

130215
this.cleanupFloating();
131216

132-
if (this.emojiPickerWrapperRef.value) {
133-
this.emojiPickerWrapperRef.value.style.zIndex = '1000';
134-
}
135-
136217
this.cleanupAutoUpdate = autoUpdate(
137218
this.buttonRef.value,
138-
this.emojiPickerWrapperRef.value,
219+
sharedEmojiPanel.wrapper,
139220
() => this.updatePosition()
140221
);
141222

@@ -144,7 +225,7 @@ export class EmojiButton extends LitElement {
144225

145226
async updatePosition() {
146227
const reference = this.buttonRef.value;
147-
const floating = this.emojiPickerWrapperRef.value;
228+
const floating = sharedEmojiPanel.wrapper;
148229

149230
if (!reference || !floating) return;
150231

@@ -182,12 +263,7 @@ export class EmojiButton extends LitElement {
182263
@click=${this.handleOpenEmojiPicker}
183264
></div>`
184265
}
185-
</div>
186-
<div
187-
class="form__emoji-panel"
188-
?hidden=${!this.emojiPickerVisible}
189-
${ref(this.emojiPickerWrapperRef)}
190-
></div>`;
266+
</div>`;
191267
}
192268

193269
static override styles = [
@@ -196,36 +272,6 @@ export class EmojiButton extends LitElement {
196272
:host {
197273
display: inline-flex;
198274
}
199-
200-
em-emoji-picker {
201-
--rgb-color: var(--halo-cw-emoji-picker-rgb-color, 34, 36, 39);
202-
--rgb-accent: var(--halo-cw-emoji-picker-rgb-accent, 34, 102, 237);
203-
--rgb-background: var(--halo-cw-emoji-picker-rgb-background, 255, 255, 255);
204-
--rgb-input: var(--halo-cw-emoji-picker-rgb-input, 255, 255, 255);
205-
--color-border: var(--halo-cw-emoji-picker-color-border, rgba(0, 0, 0, .05));
206-
--color-border-over: var(--halo-cw-emoji-picker-color-border-over, rgba(0, 0, 0, .1));
207-
}
208-
209-
.form__emoji-panel {
210-
position: absolute;
211-
box-shadow: 0 0.5em 1em rgba(0, 0, 0, 0.1);
212-
border-radius: 0.875em;
213-
overflow: hidden;
214-
animation: fadeInUp 0.3s both;
215-
z-index: 1000;
216-
}
217-
218-
@keyframes fadeInUp {
219-
from {
220-
opacity: 0;
221-
transform: translate3d(0, 5%, 0);
222-
}
223-
224-
to {
225-
opacity: 1;
226-
transform: translate3d(0, 0, 0);
227-
}
228-
}
229275
@unocss-placeholder;
230276
`,
231277
];

0 commit comments

Comments
 (0)