MM-61611 Fix layout of emoji picker in mobile view (#30747)

* Stop emoji picker header from overlapping tabs

* Move emoji picker header above tabs

There was also some stuff I could remove because the header will always
be visible.

* Don't scroll the emoji picker on hover

* MM-61611 Fix emoji picker tabs being covered in mobile view

* Improve layout of emoji picker and gif picker

A lot of the emoji picker was explicitly sized before which made it
difficult to make everything responsive in mobile view. Making the emoji
picker layout into a flexbox fixed that, but due to the way that it's
nested, that was a bit tricky, and it required a couple tricks:

1. The AutoSizer doesn't work if it's placed inside a flexbox container,
   but you can fix that by putting a div with position: relative in
   between it and the flexbox parent. That was already the case, but I
   accidentally removed it while trying to sort out the seemingly
   excessive nesting.
2. Children of flexbox elements will cause the parent to expand if they
   don't have min-height/min-width set. That's because the default
   value for those is 0 outside of flexbox, but it's auto inside of one.

This breaks the layout and animation of the skin tone selector in the
emoji picker, but that has issues on master as well, so it'll be
addressed in the next commit.

* Improve and fix opening animation for skin tone selector

While this looked worse before in mobile view, it had some weirdness in
desktop as well. I ended up rewriting the way that that animation worked
so that I could position it correctly in both mobile (where it doesn't
cover the input) and desktop (where it does).

Notably, that change required:

1. Changing the skin tone selector to wait until its contents are hidden
   to unmount them so that the position and size could be calculated
   correctly.
2. Removed the flexbox from the search row and used a margin to make the
   skin tone button pretend to offset the input. That let us smoothy
   expand the selector over the input without having the input resize
   behind it.

* Update CSS for positioning emojis and headings in emoji picker
Этот коммит содержится в:
Harrison Healey
2025-04-25 12:28:31 -04:00
коммит произвёл GitHub
родитель 291d5a2ba7
Коммит 80b61ad79b
14 изменённых файлов: 234 добавлений и 265 удалений

Просмотреть файл

@@ -3,155 +3,150 @@
exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
<DocumentFragment>
<div
class="emoji-picker__inner"
role="application"
aria-live="assertive"
class="sr-only"
>
grinning emoji
</div>
<div
class="emoji-picker__search-container"
>
<div
aria-live="assertive"
class="sr-only"
class="emoji-picker__text-container"
>
grinning emoji
<span
class="icon-magnify icon emoji-picker__search-icon"
/>
<input
aria-label="Search for an emoji"
autocomplete="off"
class="emoji-picker__search"
data-testid="emojiInputSearch"
id="emojiPickerSearch"
placeholder="Search emojis"
type="text"
value=""
/>
</div>
<div />
</div>
<div
class="emoji-picker__categories"
data-testid="emojiPickerCategories"
id="emojiPickerCategories"
>
<button
aria-label="Smileys & Emotion"
aria-pressed="true"
class="style--none emoji-picker__category emoji-picker__category--selected"
>
<i
class="icon-emoticon-happy-outline"
/>
</button>
<button
aria-label="People & Body"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-account-outline"
/>
</button>
<button
aria-label="Animals & Nature"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-leaf-outline"
/>
</button>
<button
aria-label="Food & Drink"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-food-apple"
/>
</button>
<button
aria-label="Travel & Places"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-airplane-variant"
/>
</button>
<button
aria-label="Activities"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-basketball"
/>
</button>
<button
aria-label="Objects"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-lightbulb-outline"
/>
</button>
<button
aria-label="Symbols"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-heart-outline"
/>
</button>
<button
aria-label="Flags"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-flag-outline"
/>
</button>
<button
aria-label="Custom"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-emoticon-custom-outline"
/>
</button>
</div>
<div
class="emoji-picker__items"
style="height: 290px;"
>
<div
class="emoji-picker__search-container"
aria-labelledby="emojiPickerSearch"
class="emoji-picker__container"
role="grid"
>
<div
class="emoji-picker__text-container"
>
<span
class="icon-magnify icon emoji-picker__search-icon"
/>
<input
aria-label="Search for an emoji"
autocomplete="off"
class="emoji-picker__search"
data-testid="emojiInputSearch"
id="emojiPickerSearch"
placeholder="Search emojis"
type="text"
value=""
/>
</div>
<div />
style="overflow: visible; height: 0px; width: 0px;"
/>
</div>
</div>
<div
class="emoji-picker__footer"
>
<div
class="emoji-picker__categories"
data-testid="emojiPickerCategories"
id="emojiPickerCategories"
class="emoji-picker__preview"
>
<button
aria-label="Smileys & Emotion"
aria-pressed="true"
class="style--none emoji-picker__category emoji-picker__category--selected"
>
<i
class="icon-emoticon-happy-outline"
/>
</button>
<button
aria-label="People & Body"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-account-outline"
/>
</button>
<button
aria-label="Animals & Nature"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-leaf-outline"
/>
</button>
<button
aria-label="Food & Drink"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-food-apple"
/>
</button>
<button
aria-label="Travel & Places"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-airplane-variant"
/>
</button>
<button
aria-label="Activities"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-basketball"
/>
</button>
<button
aria-label="Objects"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-lightbulb-outline"
/>
</button>
<button
aria-label="Symbols"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-heart-outline"
/>
</button>
<button
aria-label="Flags"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-flag-outline"
/>
</button>
<button
aria-label="Custom"
aria-pressed="false"
class="style--none emoji-picker__category"
>
<i
class="icon-emoticon-custom-outline"
/>
</button>
</div>
<div
class="emoji-picker__items"
style="height: 290px;"
>
<div
aria-labelledby="emojiPickerSearch"
class="emoji-picker__container"
role="grid"
>
<div
style="overflow: visible; height: 0px; width: 0px;"
/>
</div>
</div>
<div
class="emoji-picker__footer"
>
<div
class="emoji-picker__preview"
>
Preview for grinning emoji
</div>
Preview for grinning emoji
</div>
</div>
</DocumentFragment>

Просмотреть файл

@@ -2,7 +2,7 @@
exports[`components/emoji_picker/components/EmojiPickerHeader handleEmojiPickerClose, should have called props.handleEmojiPickerClose 1`] = `
<div
className="emoji-picker__header modal-header"
className="emoji-picker__header"
>
<button
className="close emoji-picker__header-close-button"
@@ -36,7 +36,7 @@ exports[`components/emoji_picker/components/EmojiPickerHeader handleEmojiPickerC
exports[`components/emoji_picker/components/EmojiPickerHeader should match snapshot, 1`] = `
<div
className="emoji-picker__header modal-header"
className="emoji-picker__header"
>
<button
className="close emoji-picker__header-close-button"

Просмотреть файл

@@ -15,7 +15,7 @@ interface Props {
function EmojiPickerCategoryRow({categoryName, style}: Props) {
return (
<div
className='emoji-picker-items__container'
className='emoji-picker__row'
style={style}
role='row'
>

Просмотреть файл

@@ -11,7 +11,7 @@ type Props = {
const EmojiPickerHeader = ({handleEmojiPickerClose}: Props) => {
return (
<div className='emoji-picker__header modal-header'>
<div className='emoji-picker__header'>
<button
type='button'
className='close emoji-picker__header-close-button'

Просмотреть файл

@@ -80,6 +80,7 @@ export type Props = {
type State = {
pickerExtended: boolean;
pickerMounted: boolean;
}
export class EmojiPickerSkin extends React.PureComponent<Props, State> {
@@ -88,6 +89,7 @@ export class EmojiPickerSkin extends React.PureComponent<Props, State> {
this.state = {
pickerExtended: false,
pickerMounted: false,
};
}
@@ -109,7 +111,16 @@ export class EmojiPickerSkin extends React.PureComponent<Props, State> {
};
showSkinTonePicker = () => {
this.setState({pickerExtended: true});
this.setState({
pickerExtended: true,
pickerMounted: true,
});
};
handleSkinToneHidden = () => {
this.setState({
pickerMounted: false,
});
};
skinTonePickerButton() {
@@ -194,16 +205,17 @@ export class EmojiPickerSkin extends React.PureComponent<Props, State> {
return (
<CSSTransition
in={this.state.pickerExtended}
onExited={this.handleSkinToneHidden}
classNames='skin-tones-animation'
timeout={200}
>
<div className={classNames('skin-tones', {'skin-tones--active': this.state.pickerExtended})}>
<div className={classNames('skin-tones', {'skin-tones--active': this.state.pickerMounted})}>
<div
className={classNames('skin-tones__content', {'skin-tones__content__single': !this.state.pickerExtended}, {'skin-tones__close': this.state.pickerExtended})}
className={classNames('skin-tones__content', {'skin-tones__content__single': !this.state.pickerMounted}, {'skin-tones__close': this.state.pickerMounted})}
aria-orientation='horizontal'
>
{this.skinTonePickerButton()}
{this.state.pickerExtended &&
{this.state.pickerMounted &&
<>
<div className='skin-tones__close-text'>
<FormattedMessage

Просмотреть файл

@@ -147,14 +147,6 @@ const EmojiPicker = ({
infiniteLoaderRef?.current?._listRef?.scrollToItem(0, 'start');
}, [filter]);
// scroll as little as possible on cursor navigation
useEffect(() => {
if (cursor.emojiId) {
// eslint-disable-next-line no-underscore-dangle
infiniteLoaderRef?.current?._listRef?.scrollToItem(cursor.rowIndex, 'auto');
}
}, [cursor.rowIndex]);
const focusOnSearchInput = useCallback(() => {
searchInputRef.current?.focus();
}, []);
@@ -209,6 +201,9 @@ const EmojiPicker = ({
emojiId: '',
emoji: undefined,
});
// eslint-disable-next-line no-underscore-dangle
infiniteLoaderRef.current?._listRef?.scrollTo(0);
}, []);
const onAddCustomEmojiClickInner = useCallback(() => {
@@ -348,6 +343,9 @@ const EmojiPicker = ({
emojiId: newCursor.emojiId,
emoji: newCursorEmoji,
});
// eslint-disable-next-line no-underscore-dangle
infiniteLoaderRef?.current?._listRef?.scrollToItem(newCursor.rowIndex, 'auto');
};
const handleEnterOnEmoji = useCallback(() => {
@@ -378,10 +376,7 @@ const EmojiPicker = ({
const areSearchResultsEmpty = filter.length !== 0 && categoryOrEmojisRows.length === 1 && categoryOrEmojisRows?.[0]?.items?.[0]?.categoryName === SEARCH_RESULTS;
return (
<div
className='emoji-picker__inner'
role='application'
>
<>
<div
aria-live='assertive'
className='sr-only'
@@ -456,7 +451,7 @@ const EmojiPicker = ({
onClick={onAddCustomEmojiClickInner}
/>
</div>
</div>
</>
);
};

Просмотреть файл

@@ -42,6 +42,7 @@ export default function EmojiPickerTabs(props: Props) {
aria-label={activeKey === 1 ? intl.formatMessage({id: 'emoji_gif_picker.dialog.emojis', defaultMessage: 'Emoji Picker'}) : intl.formatMessage({id: 'emoji_gif_picker.dialog.gifs', defaultMessage: 'GIF Picker'})}
aria-modal='true'
>
<EmojiPickerHeader handleEmojiPickerClose={props.onEmojiClose}/>
<Tabs
id='emoji-picker-tabs'
defaultActiveKey={1}
@@ -51,7 +52,6 @@ export default function EmojiPickerTabs(props: Props) {
activeKey={activeKey}
onSelect={(activeKey) => setActiveKey(activeKey)}
>
<EmojiPickerHeader handleEmojiPickerClose={props.onEmojiClose}/>
<Tab
eventKey={1}
title={

Просмотреть файл

@@ -44,7 +44,7 @@ function GifPickerSearch(props: Props) {
return (
<div className='emoji-picker__search-container'>
<div className='emoji-picker__text-container'>
<div className='emoji-picker__text-container gif-picker__text-container'>
<span className='icon-magnify icon emoji-picker__search-icon'/>
<input
id='emojiPickerSearch'

Просмотреть файл

@@ -34,7 +34,7 @@ const GifPicker = (props: Props) => {
}, [props.getRootPickerNode]);
return (
<div>
<>
<GifPickerSearch
value={props.filter}
onChange={props.handleFilterChange}
@@ -44,7 +44,7 @@ const GifPicker = (props: Props) => {
filter={props.filter}
onClick={handleItemClick}
/>
</div>
</>
);
};

Просмотреть файл

@@ -70,3 +70,35 @@
.custom-emoji-tab__icon {
margin-right: 7px;
}
#emoji-picker-tabs {
display: flex;
// These prevent a flex element from overflowing its parent
min-width: 0;
min-height: 0;
flex: 1;
flex-direction: column;
.tab-content {
display: flex;
// These prevent a flex element from overflowing its parent
min-width: 0;
min-height: 0;
flex: 1;
.tab-pane {
display: flex;
// These prevent a flex element from overflowing its parent
min-width: 0;
min-height: 0;
flex: 1;
flex-direction: column;
}
}
}

Просмотреть файл

@@ -100,16 +100,6 @@
@include mixins.clearfix;
.emoji-picker__header {
background: var(--sidebar-header-bg);
}
&.emoji-picker--single {
.emoji-picker__header {
position: relative;
}
}
.browser--ie & {
width: 325px;
}
@@ -135,11 +125,11 @@
}
a {
height: 40px;
padding: 7px 12px;
border: none !important;
border-bottom: 2px solid transparent !important;
margin-right: 5px;
margin-bottom: 0;
margin-left: 5px;
background: transparent;
outline: none;
@@ -153,8 +143,6 @@
&.active {
a {
height: 34px;
padding-top: 9px;
border-radius: 0;
margin-right: 5px;
margin-left: 5px;
@@ -226,15 +214,12 @@
}
}
.emoji-picker__header,
.modal-body .emoji-picker .emoji-picker__header {
position: absolute;
.emoji-picker__header {
top: 0;
display: none; // only visible < 480px width screen
width: 100%;
padding: 13px 10px 13px 15px;
border: 1px solid variables.$light-gray;
background: variables.$primary-color;
background: var(--sidebar-header-bg);
color: var(--sidebar-header-text-color);
.emoji-picker__header-title {
@@ -255,23 +240,16 @@
}
}
.modal-body .emoji-picker .emoji-picker__header {
.emoji-picker__header-title {
margin-top: 0;
}
}
.emoji-picker__search-container {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
border-color: transparent !important;
margin: 12px;
.skin-tones-animation {
&-enter {
transform: translateX(100%);
transform: translateX(calc(100% - 32px));
}
&-enter-active {
@@ -279,30 +257,34 @@
transition: transform 200ms ease-in-out;
}
&-enter-done {
transform: translateX(0%);
}
&-exit {
position: absolute;
width: 100%;
transform: translateX(0%);
}
&-exit-active {
transform: translateX(90%);
transform: translateX(calc(100% - 32px));
transition: transform 200ms ease-in-out;
}
}
.skin-tones {
position: absolute;
z-index: 10;
right: 0;
bottom: 0;
display: flex;
height: 100%;
align-items: center;
background: var(--center-channel-bg);
margin-inline-start: 8px;
&--active {
position: absolute;
width: 100%;
justify-content: space-around;
width: fit-content;
justify-content: flex-start;
.skin-tones__close,
.skin-tones__icons {
@@ -351,13 +333,15 @@
}
.emoji-picker__text-container {
position: relative;
z-index: 5;
width: 100%;
height: 32px;
flex: 1;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
border-radius: 4px;
&:not(.gif-picker__text-container) {
margin-right: 40px;
}
&:focus-within {
border: 2px solid functions.v(button-bg);
@@ -459,8 +443,8 @@
}
.gif-attribution {
padding: 5px 0 5px 5px;
margin-inline-start: 8px;
padding-inline-start: 5px;
& > img {
width: 100%;
@@ -476,14 +460,13 @@ $emoji-footer-height: $emoji-footer-border-width + $emoji-half-height + $emoji-
.emoji-picker__items {
position: relative;
width: 100%;
height: 270px;
padding: 4px 0 4px 12px;
border-top-width: 1px;
border-top-style: solid;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
overflow-y: auto;
padding-block: 4px;
padding-inline: 0px;
&:has(.no-results__wrapper) {
display: flex;
@@ -507,11 +490,11 @@ $emoji-footer-height: $emoji-footer-border-width + $emoji-half-height + $emoji-
.emoji-picker__category-header {
position: relative;
padding-top: 3px;
margin-top: 6px;
align-content: center;
color: rgba(var(--center-channel-color-rgb), 0.48);
font-size: 12px;
font-weight: bold;
line-height: normal;
text-transform: uppercase;
}
@@ -531,6 +514,8 @@ $emoji-footer-height: $emoji-footer-border-width + $emoji-half-height + $emoji-
.emoji-picker__row {
display: flex;
justify-content: flex-start;
padding-inline-end: 4px;
padding-inline-start: 12px;
}
.emoji-picker__item {
@@ -578,10 +563,6 @@ $emoji-footer-height: $emoji-footer-border-width + $emoji-half-height + $emoji-
background-color: rgba(var(--center-channel-color-rgb), 0.32);
}
> div {
text-align: center;
}
img {
position: relative;
transition: transform 0.2s ease-in-out;

Просмотреть файл

@@ -378,41 +378,19 @@
}
.emoji-picker {
// !important is used to overide inline styles
// used on larger screens
top: 0 !important;
right: auto !important;
bottom: auto !important;
left: 0 !important;
width: 100%;
height: 100%;
display: flex;
width: 100vw;
height: 100vh;
flex-direction: column;
border-radius: 0;
>ul {
margin-top: 66px;
}
margin-right: 0;
.search-grid-container {
height: calc(100vh - 170px);
}
}
.emoji-picker__inner {
display: flex;
height: calc(100vh - 100px);
flex-direction: column;
.emoji-picker--single & {
height: calc(100vh - 55px);
}
>* {
flex-grow: 0;
}
}
.emoji-picker__header,
.modal-body .emoji-picker .emoji-picker__header {
.emoji-picker__header {
display: block;
}
@@ -423,19 +401,10 @@
}
}
.emoji-picker__search-container {
height: 40px;
.emoji-picker__text-container {
width: 95%;
}
}
.emoji-picker__items {
flex-grow: 1;
flex: 1;
.emoji-picker__category-header {
padding-top: 8px;
font-size: 14px;
}
}
@@ -444,20 +413,7 @@
width: calc(100% / 15);
height: auto;
padding: 0;
padding-top: calc(100% / 15);
margin: calc(100% / 45);
> div {
position: absolute;
top: 50%;
left: 50%;
display: flex;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
}
margin-inline: calc(100% / 45);
}
.emoji-picker__preview {
@@ -1815,7 +1771,6 @@
.emoji-picker__item {
width: calc(100% / #{variables.$emoji-per-row});
padding-top: calc(100% / #{variables.$emoji-per-row});
margin: 0;
}

Просмотреть файл

@@ -24,7 +24,7 @@ $announcement-bar-height: 40px;
$backstage-bar-height: 43px;
// Random variables
$emoji-per-row: 9; // needs to match variable `EMOJI_PER_ROW` in emoji_picker.jsx
$emoji-per-row: 9; // needs to match variable `EMOJI_PER_ROW` in components/emoji_picker/constants/index.ts
// Transition timing defaults
$transition-quick: 0.12s;

Просмотреть файл

@@ -323,7 +323,6 @@ export function applyTheme(theme: Theme) {
changeCss('.app__body .channel-header .pinned-posts-button svg', 'fill:' + changeOpacity(theme.centerChannelColor, 0.75));
changeCss('.app__body .channel-header .channel-header_plugin-dropdown svg', 'fill:' + changeOpacity(theme.centerChannelColor, 0.75));
changeCss('.app__body .file-preview, .app__body .post-image__details, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .webhooks__container, .app__body .dropdown-menu', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.emoji-picker .emoji-picker__header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.app__body .popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25));
changeCss('.app__body .btn.btn-transparent', 'color:' + changeOpacity(theme.centerChannelColor, 0.7));
changeCss('.app__body .popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25));
@@ -366,7 +365,7 @@ export function applyTheme(theme: Theme) {
changeCss('.app__body .emoji-picker', 'color:' + theme.centerChannelColor);
changeCss('.app__body .emoji-picker', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.app__body .emoji-picker__search-icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.4));
changeCss('.app__body .emoji-picker__preview, .app__body .emoji-picker__items, .app__body .emoji-picker__search-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.app__body .emoji-picker__preview, .app__body .emoji-picker__items', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
changeCss('.emoji-picker__category .fa:hover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));
changeCss('.app__body .emoji-picker__item-wrapper:hover', 'background-color:' + changeOpacity(theme.centerChannelColor, 0.8));
changeCss('.app__body .icon__postcontent_picker:hover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8));