Adds keyboard affordance to story viewer

This commit is contained in:
Josh Perez 2022-04-07 17:11:33 -04:00 committed by GitHub
parent 09fbfe5421
commit 19bb3bc994
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 224 additions and 196 deletions

View file

@ -10,6 +10,12 @@
height: 96px; height: 96px;
width: 100%; width: 100%;
@include keyboard-mode {
&:focus {
background: $color-gray-65;
}
}
&:hover { &:hover {
background: $color-gray-65; background: $color-gray-65;
} }

View file

@ -40,6 +40,12 @@
width: 24px; width: 24px;
@include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white); @include color-svg('../images/icons/v2/more-horiz-24.svg', $color-white);
@include keyboard-mode {
&:focus {
background-color: $color-ultramarine;
}
}
} }
&__container { &__container {
@ -89,6 +95,11 @@
&__reply { &__reply {
@include button-reset; @include button-reset;
@include keyboard-mode {
&:focus {
color: $color-ultramarine;
}
}
} }
&__progress { &__progress {

View file

@ -1,6 +1,7 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import FocusTrap from 'focus-trap-react';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@ -82,51 +83,54 @@ export const StoriesPane = ({
}, [searchTerm, stories]); }, [searchTerm, stories]);
return ( return (
<> <FocusTrap>
<div className="Stories__pane__header"> <div>
<button <div className="Stories__pane__header">
aria-label={i18n('back')} <button
className="Stories__pane__header--back" aria-label={i18n('back')}
onClick={onBack} className="Stories__pane__header--back"
type="button" onClick={onBack}
tabIndex={0}
type="button"
/>
<div className="Stories__pane__header--title">
{i18n('Stories__title')}
</div>
</div>
<SearchInput
i18n={i18n}
moduleClassName="Stories__search"
onChange={event => {
setSearchTerm(event.target.value);
}}
placeholder={i18n('search')}
value={searchTerm}
/> />
<div className="Stories__pane__header--title"> <div
{i18n('Stories__title')} className={classNames('Stories__pane__list', {
'Stories__pane__list--empty': !stories.length,
})}
>
{renderedStories.map(story => (
<StoryListItem
key={getNewestStory(story).timestamp}
i18n={i18n}
onClick={() => {
onStoryClicked(story.conversationId);
}}
onHideStory={() => {
toggleHideStories(getNewestStory(story).sender.id);
}}
onGoToConversation={conversationId => {
openConversationInternal({ conversationId });
}}
queueStoryDownload={queueStoryDownload}
story={getNewestStory(story)}
/>
))}
{!stories.length && i18n('Stories__list-empty')}
</div> </div>
</div> </div>
<SearchInput </FocusTrap>
i18n={i18n}
moduleClassName="Stories__search"
onChange={event => {
setSearchTerm(event.target.value);
}}
placeholder={i18n('search')}
value={searchTerm}
/>
<div
className={classNames('Stories__pane__list', {
'Stories__pane__list--empty': !stories.length,
})}
>
{renderedStories.map(story => (
<StoryListItem
key={getNewestStory(story).timestamp}
i18n={i18n}
onClick={() => {
onStoryClicked(story.conversationId);
}}
onHideStory={() => {
toggleHideStories(getNewestStory(story).sender.id);
}}
onGoToConversation={conversationId => {
openConversationInternal({ conversationId });
}}
queueStoryDownload={queueStoryDownload}
story={getNewestStory(story)}
/>
))}
{!stories.length && i18n('Stories__list-empty')}
</div>
</>
); );
}; };

View file

@ -124,6 +124,7 @@ export const StoryListItem = ({
} }
}} }}
ref={setReferenceElement} ref={setReferenceElement}
tabIndex={0}
type="button" type="button"
> >
<Avatar <Avatar

View file

@ -1,6 +1,7 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import FocusTrap from 'focus-trap-react';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSpring, animated, to } from '@react-spring/web'; import { useSpring, animated, to } from '@react-spring/web';
import type { BodyRangeType, LocalizerType } from '../types/Util'; import type { BodyRangeType, LocalizerType } from '../types/Util';
@ -197,170 +198,175 @@ export const StoryViewer = ({
}, [navigateStories]); }, [navigateStories]);
return ( return (
<div className="StoryViewer"> <FocusTrap>
<div className="StoryViewer__overlay" /> <div className="StoryViewer">
<div className="StoryViewer__content"> <div className="StoryViewer__overlay" />
<button <div className="StoryViewer__content">
aria-label={i18n('MyStories__more')} <button
className="StoryViewer__more" aria-label={i18n('MyStories__more')}
type="button" className="StoryViewer__more"
/> tabIndex={0}
<button type="button"
aria-label={i18n('close')}
className="StoryViewer__close-button"
onClick={onClose}
type="button"
/>
<div className="StoryViewer__container">
<StoryImage
attachment={attachment}
i18n={i18n}
label={i18n('lightboxImageAlt')}
moduleClassName="StoryViewer__story"
queueStoryDownload={queueStoryDownload}
storyId={messageId}
/> />
<div className="StoryViewer__meta"> <button
<Avatar aria-label={i18n('close')}
acceptedMessageRequest={acceptedMessageRequest} className="StoryViewer__close-button"
avatarPath={avatarPath} onClick={onClose}
badge={undefined} tabIndex={0}
color={getAvatarColor(color)} type="button"
conversationType="direct" />
<div className="StoryViewer__container">
<StoryImage
attachment={attachment}
i18n={i18n} i18n={i18n}
isMe={Boolean(isMe)} label={i18n('lightboxImageAlt')}
name={name} moduleClassName="StoryViewer__story"
profileName={profileName} queueStoryDownload={queueStoryDownload}
sharedGroupNames={sharedGroupNames} storyId={messageId}
size={AvatarSize.TWENTY_EIGHT}
title={title}
/> />
{group && ( <div className="StoryViewer__meta">
<Avatar <Avatar
acceptedMessageRequest={group.acceptedMessageRequest} acceptedMessageRequest={acceptedMessageRequest}
avatarPath={group.avatarPath} avatarPath={avatarPath}
badge={undefined} badge={undefined}
className="StoryViewer__meta--group-avatar" color={getAvatarColor(color)}
color={getAvatarColor(group.color)} conversationType="direct"
conversationType="group"
i18n={i18n} i18n={i18n}
isMe={false} isMe={Boolean(isMe)}
name={group.name} name={name}
profileName={group.profileName} profileName={profileName}
sharedGroupNames={group.sharedGroupNames} sharedGroupNames={sharedGroupNames}
size={AvatarSize.TWENTY_EIGHT} size={AvatarSize.TWENTY_EIGHT}
title={group.title} title={title}
/> />
)} {group && (
<div className="StoryViewer__meta--title"> <Avatar
{group acceptedMessageRequest={group.acceptedMessageRequest}
? i18n('Stories__from-to-group', { avatarPath={group.avatarPath}
name: title, badge={undefined}
group: group.title, className="StoryViewer__meta--group-avatar"
}) color={getAvatarColor(group.color)}
: title} conversationType="group"
</div> i18n={i18n}
<MessageTimestamp isMe={false}
i18n={i18n} name={group.name}
module="StoryViewer__meta--timestamp" profileName={group.profileName}
timestamp={timestamp} sharedGroupNames={group.sharedGroupNames}
/> size={AvatarSize.TWENTY_EIGHT}
<div className="StoryViewer__progress"> title={group.title}
{stories.map((story, index) => ( />
<div )}
className="StoryViewer__progress--container" <div className="StoryViewer__meta--title">
key={story.messageId} {group
> ? i18n('Stories__from-to-group', {
{currentStoryIndex === index ? ( name: title,
<animated.div group: group.title,
className="StoryViewer__progress--bar" })
style={{ : title}
width: to([styles.width], width => `${width}%`), </div>
}} <MessageTimestamp
/> i18n={i18n}
) : ( module="StoryViewer__meta--timestamp"
<div timestamp={timestamp}
className="StoryViewer__progress--bar" />
style={{ <div className="StoryViewer__progress">
width: currentStoryIndex < index ? '0%' : '100%', {stories.map((story, index) => (
}} <div
/> className="StoryViewer__progress--container"
)} key={story.messageId}
</div> >
))} {currentStoryIndex === index ? (
<animated.div
className="StoryViewer__progress--bar"
style={{
width: to([styles.width], width => `${width}%`),
}}
/>
) : (
<div
className="StoryViewer__progress--bar"
style={{
width: currentStoryIndex < index ? '0%' : '100%',
}}
/>
)}
</div>
))}
</div>
</div> </div>
</div> </div>
<div className="StoryViewer__actions">
{isMe ? (
<>
{views &&
(views === 1 ? (
<Intl
i18n={i18n}
id="MyStories__views--singular"
components={[<strong>{views}</strong>]}
/>
) : (
<Intl
i18n={i18n}
id="MyStories__views--plural"
components={[<strong>{views}</strong>]}
/>
))}
{views && replies && ' '}
{replies &&
(replies === 1 ? (
<Intl
i18n={i18n}
id="MyStories__replies--singular"
components={[<strong>{replies}</strong>]}
/>
) : (
<Intl
i18n={i18n}
id="MyStories__replies--plural"
components={[<strong>{replies}</strong>]}
/>
))}
</>
) : (
<button
className="StoryViewer__reply"
onClick={() => setHasReplyModal(true)}
tabIndex={0}
type="button"
>
{i18n('StoryViewer__reply')}
</button>
)}
</div>
</div> </div>
<div className="StoryViewer__actions"> {hasReplyModal && (
{isMe ? ( <StoryViewsNRepliesModal
<> authorTitle={title}
{views && getPreferredBadge={getPreferredBadge}
(views === 1 ? ( i18n={i18n}
<Intl isMyStory={isMe}
i18n={i18n} onClose={() => setHasReplyModal(false)}
id="MyStories__views--singular" onReact={emoji => {
components={[<strong>{views}</strong>]} onReactToStory(emoji, visibleStory);
/> }}
) : ( onReply={(message, mentions, replyTimestamp) => {
<Intl setHasReplyModal(false);
i18n={i18n} onReplyToStory(message, mentions, replyTimestamp, visibleStory);
id="MyStories__views--plural" }}
components={[<strong>{views}</strong>]} onSetSkinTone={onSetSkinTone}
/> onTextTooLong={onTextTooLong}
))} onUseEmoji={onUseEmoji}
{views && replies && ' '} preferredReactionEmoji={preferredReactionEmoji}
{replies && recentEmojis={recentEmojis}
(replies === 1 ? ( renderEmojiPicker={renderEmojiPicker}
<Intl replies={[]}
i18n={i18n} skinTone={skinTone}
id="MyStories__replies--singular" storyPreviewAttachment={attachment}
components={[<strong>{replies}</strong>]} views={[]}
/> />
) : ( )}
<Intl
i18n={i18n}
id="MyStories__replies--plural"
components={[<strong>{replies}</strong>]}
/>
))}
</>
) : (
<button
className="StoryViewer__reply"
onClick={() => setHasReplyModal(true)}
type="button"
>
{i18n('StoryViewer__reply')}
</button>
)}
</div>
</div> </div>
{hasReplyModal && ( </FocusTrap>
<StoryViewsNRepliesModal
authorTitle={title}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
isMyStory={isMe}
onClose={() => setHasReplyModal(false)}
onReact={emoji => {
onReactToStory(emoji, visibleStory);
}}
onReply={(message, mentions, replyTimestamp) => {
setHasReplyModal(false);
onReplyToStory(message, mentions, replyTimestamp, visibleStory);
}}
onSetSkinTone={onSetSkinTone}
onTextTooLong={onTextTooLong}
onUseEmoji={onUseEmoji}
preferredReactionEmoji={preferredReactionEmoji}
recentEmojis={recentEmojis}
renderEmojiPicker={renderEmojiPicker}
replies={[]}
skinTone={skinTone}
storyPreviewAttachment={attachment}
views={[]}
/>
)}
</div>
); );
}; };