[MM-61577]: Ensure all interactive functionality is operable with the keyboard in emoji picker (#29629)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
316cde2569
Коммит
e8e40c79f6
@@ -128,11 +128,11 @@ describe('Custom emojis', () => {
|
||||
cy.findByText('Custom').should('exist').and('is.visible');
|
||||
|
||||
// * Verify that first custom emoji exists and is visible to user
|
||||
cy.findAllByAltText('custom emoji image').should('exist').eq(0).and('is.visible');
|
||||
cy.findAllByAltText('custom emoji').should('exist').eq(0).and('is.visible');
|
||||
|
||||
// * Verify second custom emoji exists and is visible to user,
|
||||
// if both custom emojis are visible we can conclude that they are not overlapping
|
||||
cy.findAllByAltText('custom emoji image').should('exist').eq(1).and('is.visible');
|
||||
cy.findAllByAltText('custom emoji').should('exist').eq(1).and('is.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,10 +69,10 @@ describe('Recent Emoji', () => {
|
||||
cy.findByText(/Recently Used/i).should('exist').and('be.visible');
|
||||
|
||||
// * Assert first emoji should equal with second recent emoji
|
||||
cy.findAllByTestId('emojiItem').eq(0).find('img').should('have.attr', 'aria-label', 'grin emoji');
|
||||
cy.findAllByTestId('emojiItem').eq(0).should('have.attr', 'aria-label', 'grin emoji');
|
||||
|
||||
// * Assert second emoji should equal with first recent emoji
|
||||
cy.findAllByTestId('emojiItem').eq(1).find('img').should('have.attr', 'aria-label', 'joy emoji');
|
||||
cy.findAllByTestId('emojiItem').eq(1).should('have.attr', 'aria-label', 'joy emoji');
|
||||
});
|
||||
|
||||
it('MM-T4463 Recently used custom emoji, when is deleted should be removed from recent emoji category and quick reactions', () => {
|
||||
@@ -130,7 +130,7 @@ describe('Recent Emoji', () => {
|
||||
cy.findAllByTestId('emojiItem').eq(0).find('img').should('have.attr', 'class', 'emoji-category--custom');
|
||||
|
||||
// * Verify second most recent one is the system emoji in emoji picker
|
||||
cy.findAllByTestId('emojiItem').eq(1).find('img').should('have.attr', 'aria-label', 'lemon emoji');
|
||||
cy.findAllByTestId('emojiItem').eq(1).should('have.attr', 'aria-label', 'lemon emoji');
|
||||
|
||||
// # Go to custom emoji page
|
||||
cy.findByText('Custom Emoji').should('be.visible').click();
|
||||
@@ -172,6 +172,6 @@ describe('Recent Emoji', () => {
|
||||
cy.findByText('Recently Used').should('exist').and('be.visible');
|
||||
|
||||
// * Verify most recent one is the system emoji in emoji picker and not the custom emoji
|
||||
cy.findAllByTestId('emojiItem').eq(0).find('img').should('have.attr', 'aria-label', 'lemon emoji');
|
||||
cy.findAllByTestId('emojiItem').eq(0).should('have.attr', 'aria-label', 'lemon emoji');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('Recent Emoji', () => {
|
||||
cy.findByText('Recently Used').should('exist').and('be.visible');
|
||||
|
||||
// * Verify most recent one is the thumbsup emoji with default skin
|
||||
cy.findAllByTestId('emojiItem').eq(0).find('img').should('have.attr', 'aria-label', '+1 emoji');
|
||||
cy.findAllByTestId('emojiItem').eq(0).should('have.attr', 'aria-label', '+1 emoji');
|
||||
|
||||
// # Open skin picker again to change the skin
|
||||
cy.findByAltText('emoji skin tone picker').should('exist').parent().click().wait(TIMEOUTS.ONE_SEC);
|
||||
@@ -65,7 +65,7 @@ describe('Recent Emoji', () => {
|
||||
cy.findByTestId('skin-pick-1F3FF').should('exist').click();
|
||||
|
||||
// * Verify most recent one is the same thumbsup emoji but now with a dark skin tone
|
||||
cy.findAllByTestId('emojiItem').eq(0).find('img').should('have.attr', 'aria-label', '+1 dark skin tone emoji');
|
||||
cy.findAllByTestId('emojiItem').eq(0).should('have.attr', 'aria-label', '+1 dark skin tone emoji');
|
||||
});
|
||||
|
||||
// # Close emoji picker
|
||||
|
||||
@@ -32,8 +32,13 @@ describe('Emoji sorting', () => {
|
||||
|
||||
// # Assert first recently used emoji has the data-test-id value of 'cat' which was the last one we sent
|
||||
cy.findAllByTestId('emojiItem').
|
||||
findByRole('button', {name: 'cat emoji'}).
|
||||
should('exist');
|
||||
each(($btn) => {
|
||||
// Check if the button has the specific aria-label
|
||||
if ($btn.attr('aria-label') === 'cat emoji') {
|
||||
// If the aria-label matches, check if the button exists
|
||||
cy.wrap($btn).should('exist');
|
||||
}
|
||||
});
|
||||
|
||||
const emojiList = [];
|
||||
|
||||
|
||||
@@ -99,13 +99,13 @@ describe('MM-T154 Use keyboard navigation in emoji picker', () => {
|
||||
cy.uiGetPostTextBox().type('{enter}');
|
||||
|
||||
// * Compare selected emoji with last post
|
||||
cy.getLastPost().find('.emoticon').should('have.attr', 'alt', selectedEmoji);
|
||||
cy.getLastPost().find('.emoticon').should('have.attr', 'aria-label', selectedEmoji);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const testSelectedIndex = (done) => {
|
||||
cy.get('div.emoji-picker__item.selected').then((selectedEmoji) => {
|
||||
cy.get('button.emoji-picker__item.selected').then((selectedEmoji) => {
|
||||
done(selectedEmoji.index());
|
||||
});
|
||||
};
|
||||
|
||||
@@ -134,7 +134,9 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
|
||||
style="height: 290px;"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="emojiPickerSearch"
|
||||
class="emoji-picker__container"
|
||||
role="grid"
|
||||
>
|
||||
<div
|
||||
style="overflow: visible; height: 0px; width: 0px;"
|
||||
|
||||
@@ -35,6 +35,7 @@ function EmojiPickerCategoryOrEmojiRow({index, style, data, cursorRowIndex, curs
|
||||
<div
|
||||
style={style}
|
||||
className='emoji-picker__row'
|
||||
role='row'
|
||||
>
|
||||
{row.items.map((emojiColumn) => {
|
||||
const emoji = emojiColumn.item;
|
||||
|
||||
@@ -17,6 +17,7 @@ function EmojiPickerCategoryRow({categoryName, style}: Props) {
|
||||
<div
|
||||
className='emoji-picker-items__container'
|
||||
style={style}
|
||||
role='row'
|
||||
>
|
||||
<div
|
||||
className='emoji-picker__category-header'
|
||||
|
||||
@@ -91,7 +91,11 @@ const EmojiPickerCurrentResults = forwardRef<InfiniteLoader, Props>(({categoryOr
|
||||
className='emoji-picker__items'
|
||||
style={{height: EMOJI_CONTAINER_HEIGHT}}
|
||||
>
|
||||
<div className='emoji-picker__container'>
|
||||
<div
|
||||
className='emoji-picker__container'
|
||||
role='grid'
|
||||
aria-labelledby='emojiPickerSearch'
|
||||
>
|
||||
<AutoSizer>
|
||||
{({height, width}) => (
|
||||
<InfiniteLoader
|
||||
|
||||
@@ -55,32 +55,21 @@ function EmojiPickerItem({emoji, rowIndex, isSelected, onClick, onMouseOver}: Pr
|
||||
let content;
|
||||
|
||||
if (isSystemEmoji(emoji)) {
|
||||
const emojiName = emoji.short_name ? emoji.short_name : emoji.name;
|
||||
const emojiUnified = emoji.unified ? emoji.unified.toLowerCase() : emoji.name.toLowerCase();
|
||||
|
||||
content = (
|
||||
<img
|
||||
alt={'emoji image'}
|
||||
alt={`${emoji.name.toLocaleLowerCase()} emoji`}
|
||||
data-testid={emoji.short_names}
|
||||
src={imgTrans}
|
||||
className={`emojisprite emoji-category-${emoji.category} emoji-${emojiUnified}`}
|
||||
id={`emoji-${emojiUnified}`}
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: 'emoji_picker_item.emoji_aria_label',
|
||||
defaultMessage: '{emojiName} emoji',
|
||||
},
|
||||
{
|
||||
emojiName: (emojiName).replace(/_/g, ' '),
|
||||
},
|
||||
)}
|
||||
role='button'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<img
|
||||
alt={'custom emoji image'}
|
||||
alt={'custom emoji'}
|
||||
data-testid={emoji.name}
|
||||
src={getEmojiImageUrl(emoji)}
|
||||
className={'emoji-category--custom'}
|
||||
@@ -89,15 +78,26 @@ function EmojiPickerItem({emoji, rowIndex, isSelected, onClick, onMouseOver}: Pr
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
className={itemClassName}
|
||||
onClick={handleClick}
|
||||
onMouseOver={throttledMouseOver}
|
||||
data-testid='emojiItem'
|
||||
tabIndex={-1}
|
||||
type='button'
|
||||
id={emoji.name.toLocaleLowerCase().replaceAll(' ', '_')}
|
||||
aria-label={formatMessage(
|
||||
{
|
||||
id: 'emoji_picker_item.emoji_aria_label',
|
||||
defaultMessage: '{emojiName} emoji',
|
||||
},
|
||||
{
|
||||
emojiName: (isSystemEmoji(emoji) ? emoji.short_name : emoji.name).replace(/_/g, ' '),
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div data-testid='emojiItem'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import {useIntl} from 'react-intl';
|
||||
import {EMOJI_PER_ROW} from 'components/emoji_picker/constants';
|
||||
import {NavigationDirection} from 'components/emoji_picker/types';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
cursorCategoryIndex: number;
|
||||
@@ -24,6 +26,8 @@ interface Props {
|
||||
resetCursorPosition: () => void;
|
||||
}
|
||||
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
const EmojiPickerSearch = forwardRef<HTMLInputElement, Props>(({value, cursorCategoryIndex, cursorEmojiIndex, onChange, resetCursorPosition, onKeyDown, focus, onEnter}: Props, ref) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
@@ -39,7 +43,7 @@ const EmojiPickerSearch = forwardRef<HTMLInputElement, Props>(({value, cursorCat
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
switch (event.key) {
|
||||
case 'ArrowRight':
|
||||
case KeyCodes.RIGHT[0]:
|
||||
// If the cursor is at the end of the textbox and an emoji is currently selected, move it to the next emoji
|
||||
if ((event.currentTarget?.selectionStart ?? 0) + 1 > value.length || (cursorCategoryIndex !== -1 || cursorEmojiIndex !== -1)) {
|
||||
event.stopPropagation();
|
||||
@@ -48,7 +52,7 @@ const EmojiPickerSearch = forwardRef<HTMLInputElement, Props>(({value, cursorCat
|
||||
onKeyDown(NavigationDirection.NextEmoji);
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case KeyCodes.LEFT[0]:
|
||||
if (cursorCategoryIndex > 0 || cursorEmojiIndex > 0) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -65,7 +69,7 @@ const EmojiPickerSearch = forwardRef<HTMLInputElement, Props>(({value, cursorCat
|
||||
focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case KeyCodes.UP[0]:
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
@@ -88,7 +92,7 @@ const EmojiPickerSearch = forwardRef<HTMLInputElement, Props>(({value, cursorCat
|
||||
onKeyDown(NavigationDirection.PreviousEmojiRow);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case KeyCodes.DOWN[0]:
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
@@ -104,7 +108,14 @@ const EmojiPickerSearch = forwardRef<HTMLInputElement, Props>(({value, cursorCat
|
||||
onKeyDown(NavigationDirection.NextEmojiRow);
|
||||
}
|
||||
break;
|
||||
case 'Enter': {
|
||||
case KeyCodes.SPACE[0]: {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onEnter();
|
||||
break;
|
||||
}
|
||||
case KeyCodes.ENTER[0]: {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
@@ -340,6 +340,7 @@ const EmojiPicker = ({
|
||||
return;
|
||||
}
|
||||
|
||||
searchInputRef.current?.setAttribute('aria-activedescendant', newCursorEmoji.name.toLocaleLowerCase().replaceAll(' ', '_'));
|
||||
setCursor({
|
||||
rowIndex: newCursor.rowIndex,
|
||||
emojiId: newCursor.emojiId,
|
||||
|
||||
@@ -29,6 +29,7 @@ const PostEmoji = ({children, name, imageUrl}: Props) => {
|
||||
className='emoticon'
|
||||
data-testid={`postEmoji.${emojiText}`}
|
||||
style={{backgroundImage: backgroundImageUrl}}
|
||||
aria-label={emojiText}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
@@ -538,7 +538,9 @@ $emoji-footer-height: $emoji-footer-border-width + $emoji-half-height + $emoji-
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user