Files
backspace/packages/web/src/components/chat/EmojiPicker.tsx
T
Jannis Braun fb9fe326c4 feat(mobile): Wave 4 — RegisterPage responsive + TransferIndicator on settings + MessageInput sheets
- RegisterPage: scroll envelope, iOS-zoom-safe inputs, ≥44px tap targets, mobile-friendly invite chip + swatch row
- TransferIndicator: mounted via MobileScreenHeader.rightActions across every settings/instance screen and inline in MobileChatScreen; touch-close listener; viewport-safe panel width
- MessageInput popovers (emoji/GIF/mention) now route through new InputPopover wrapper — desktop popover, mobile bottom-sheet
- EmojiPicker mobile branch: dynamicWidth + scoped CSS (.emoji-picker-wrapper--mobile) so the <em-emoji-picker> custom element fills the sheet; bigger touch targets (perLine 8, button 40, emoji 28); desktop unchanged
- MessageInput trigger row: mobile-first sizing with md: desktop overrides (40×40 buttons + gap on mobile, 34×34 unchanged on desktop)
- Spec updates: docs/systems/auth.md (RegisterPage responsive contract), docs/systems/uploads.md (TransferIndicator mobile surfaces)
2026-05-07 22:58:57 +02:00

64 lines
2.5 KiB
TypeScript

import React, { useRef, useEffect } from 'react';
import Picker from '@emoji-mart/react';
import data from '@emoji-mart/data';
interface EmojiPickerProps {
onEmojiSelect: (emoji: { native: string }) => void;
/**
* Mobile rendering: stretch the picker to fill its container width
* (the parent bottom-sheet provides the viewport-wide bounds), use
* larger touch targets, and let the picker's own scroll area expand
* to consume the available height of the sheet.
*/
mobile?: boolean;
}
export function EmojiPicker({ onEmojiSelect, mobile = false }: EmojiPickerProps) {
const containerRef = useRef<HTMLDivElement>(null);
// Prevent keyboard events from bubbling out (e.g. Enter submitting the chat input)
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const stop = (e: KeyboardEvent) => e.stopPropagation();
el.addEventListener('keydown', stop);
return () => el.removeEventListener('keydown', stop);
}, []);
// emoji-mart's Picker computes its own internal width from
// `perLine * emojiButtonSize` UNLESS `dynamicWidth` is set, in which case
// it stretches to its parent's width. On mobile we want full-viewport.
// The mobile sheet wraps this picker in `flex-1 min-h-0 flex flex-col`,
// so we make the wrapper fill that space and tell the picker to expand.
//
// The `<em-emoji-picker>` custom element has no intrinsic stretch behavior:
// even with `dynamicWidth: true` it ships with `display: flex` but no
// `width: 100%`, so it shrinks to its perLine*emojiButtonSize content width
// unless we force it to fill. The `emoji-picker-wrapper--mobile` modifier
// applies that override (see globals.css) — desktop keeps the legacy
// intrinsic-width sizing.
const wrapperClass = mobile
? 'emoji-picker-wrapper emoji-picker-wrapper--mobile flex-1 min-h-0 w-full overflow-hidden'
: 'emoji-picker-wrapper';
return (
<div ref={containerRef} className={wrapperClass}>
<Picker
data={data}
onEmojiSelect={onEmojiSelect}
theme="dark"
set="native"
skinTonePosition="search"
previewPosition="none"
navPosition="bottom"
perLine={mobile ? 8 : 10}
maxFrequentRows={2}
emojiSize={mobile ? 28 : 24}
emojiButtonSize={mobile ? 40 : 32}
dynamicWidth={mobile ? true : false}
categories={['frequent', 'people', 'nature', 'foods', 'activity', 'places', 'objects', 'symbols', 'flags']}
/>
</div>
);
}