article navigation with keyboard

This commit is contained in:
刘浩远 2020-06-19 21:51:04 +08:00
parent 76a97852a1
commit 3578721d6c
8 changed files with 62 additions and 27 deletions

19
dist/styles.css vendored
View File

@ -631,8 +631,19 @@ img.favicon {
cursor: pointer;
animation-fill-mode: none;
}
.card:hover {
.card:hover, .card:focus {
box-shadow: #0006 0px 5px 40px;
outline: none;
}
.card:focus::after, .list-card:focus::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: calc(100% - 6px);
height: calc(100% - 6px);
border: 1px solid var(--white);
outline: 2px solid #0078d4;
}
.card:active {
transform: scale(.97);
@ -663,7 +674,8 @@ img.favicon {
.card img.head, .card p, .card h3 {
transition: transform ease-out .12s;
}
.card.transform:hover img.head, .card.transform:hover p, .card.transform:hover h3 {
.card.transform:hover img.head, .card.transform:hover p, .card.transform:hover h3,
.card.transform:focus img.head, .card.transform:focus p, .card.transform:focus h3 {
transform: translateY(-144px);
}
.card h3.title {
@ -717,8 +729,9 @@ img.favicon {
cursor: pointer;
box-shadow: #0000 0px 5px 15px;
}
.list-card:hover {
.list-card:hover, .list-card:focus {
box-shadow: #0004 0px 5px 15px;
outline: none;
}
.list-card:active {
box-shadow: #0000 0px 5px 15px, inset #0004 0px 0px 15px;

View File

@ -3,10 +3,10 @@ import intl = require("react-intl-universal")
import { renderToString } from "react-dom/server"
import { RSSItem } from "../scripts/models/item"
import { openExternal } from "../scripts/utils"
import { Stack, CommandBarButton, IContextualMenuProps } from "@fluentui/react"
import { Stack, CommandBarButton, IContextualMenuProps, FocusZone } from "@fluentui/react"
import { RSSSource, SourceOpenTarget } from "../scripts/models/source"
import { store } from "../scripts/settings"
import { clipboard } from "electron"
import { clipboard, remote } from "electron"
const FONT_SIZE_STORE_KEY = "fontSize"
const FONT_SIZE_OPTIONS = [12, 13, 14, 15, 16, 17, 18, 19, 20]
@ -28,7 +28,8 @@ type ArticleState = {
}
class Article extends React.Component<ArticleProps, ArticleState> {
webview: HTMLWebViewElement
webview: Electron.WebviewTag
shouldRefocus = false
constructor(props) {
super(props)
@ -100,14 +101,23 @@ class Article extends React.Component<ArticleProps, ArticleState> {
openExternal(event.url)
this.props.dismiss()
}
keyDownHandler = (_, input) => {
if (input.type === "keyDown" && input.key === "Escape") {
this.shouldRefocus = true
this.props.dismiss()
}
}
componentDidMount = () => {
let webview = document.getElementById("article")
let webview = document.getElementById("article") as Electron.WebviewTag
if (webview != this.webview) {
if (this.webview) this.componentWillUnmount()
webview.addEventListener("ipc-message", this.ipcHandler)
webview.addEventListener("new-window", this.popUpHandler)
webview.addEventListener("will-navigate", this.navigationHandler)
webview.addEventListener("dom-ready", () => {
let webContents = remote.webContents.fromId(webview.getWebContentsId())
webContents.on("before-input-event", this.keyDownHandler)
})
this.webview = webview
webview.focus()
}
@ -120,9 +130,10 @@ class Article extends React.Component<ArticleProps, ArticleState> {
}
componentWillUnmount = () => {
this.webview.removeEventListener("ipc-message", this.ipcHandler)
this.webview.removeEventListener("new-window", this.popUpHandler)
this.webview.removeEventListener("will-navigate", this.navigationHandler)
if (this.shouldRefocus) {
let refocus = document.querySelector("#refocus>div[tabindex='0']") as HTMLElement
if (refocus) refocus.focus()
}
}
openInBrowser = () => {
@ -144,7 +155,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
</>))) + `&s=${this.state.fontSize}&u=${this.props.item.link}`
render = () => (
<div className="article">
<FocusZone className="article">
<Stack horizontal style={{height: 36}}>
<span style={{width: 96}}></span>
<Stack className="actions" grow horizontal tokens={{childrenGap: 12}}>
@ -194,7 +205,7 @@ class Article extends React.Component<ArticleProps, ArticleState> {
src={this.state.loadWebpage ? this.props.item.link : this.articleView()}
preload={this.state.loadWebpage ? null : "article/preload.js"}
partition="sandbox" />
</div>
</FocusZone>
)
}

View File

@ -13,8 +13,12 @@ class DefaultCard extends Card {
render() {
return (
<div className={this.className()}
onClick={this.onClick} onMouseUp={this.onMouseUp} >
<div
className={this.className()}
onClick={this.onClick}
onMouseUp={this.onMouseUp}
onMouseDown={event => event.preventDefault()}
data-is-focusable>
{this.props.item.thumb ? (
<img className="bg" src={this.props.item.thumb} />
) : null}

View File

@ -12,8 +12,11 @@ class ListCard extends Card {
render() {
return (
<div className={this.className()}
onClick={this.onClick} onMouseUp={this.onMouseUp} >
<div
className={this.className()}
onClick={this.onClick} onMouseUp={this.onMouseUp}
onMouseDown={event => event.preventDefault()}
data-is-focusable>
{this.props.item.thumb ? (
<div className="head"><img src={this.props.item.thumb} /></div>
) : null}

View File

@ -2,7 +2,7 @@ import * as React from "react"
import intl = require("react-intl-universal")
import { FeedProps } from "./feed"
import DefaultCard from "../cards/default-card"
import { PrimaryButton } from 'office-ui-fabric-react';
import { PrimaryButton, FocusZone } from 'office-ui-fabric-react';
class CardsFeed extends React.Component<FeedProps> {
state = { width: window.innerWidth - 12 }
@ -31,7 +31,7 @@ class CardsFeed extends React.Component<FeedProps> {
render() {
return this.props.feed.loaded && (
<div className="cards-feed-container">
<FocusZone as="div" id="refocus" className="cards-feed-container">
{
this.props.items.map((item) => (
<DefaultCard
@ -56,7 +56,7 @@ class CardsFeed extends React.Component<FeedProps> {
{ this.props.items.length === 0 && (
<div className="empty">{intl.get("article.empty")}</div>
)}
</div>
</FocusZone>
)
}
}

View File

@ -1,13 +1,13 @@
import * as React from "react"
import intl = require("react-intl-universal")
import { FeedProps } from "./feed"
import { DefaultButton } from 'office-ui-fabric-react';
import { DefaultButton, FocusZone, FocusZoneDirection } from 'office-ui-fabric-react';
import ListCard from "../cards/list-card";
class ListFeed extends React.Component<FeedProps> {
render() {
return this.props.feed.loaded && (
<div className="list-feed">
<FocusZone as="div" id="refocus" direction={FocusZoneDirection.vertical} className="list-feed">
{
this.props.items.map((item) => (
<ListCard
@ -31,7 +31,7 @@ class ListFeed extends React.Component<FeedProps> {
{ this.props.items.length === 0 && (
<div className="empty">{intl.get("article.empty")}</div>
)}
</div>
</FocusZone>
)
}
}

View File

@ -5,7 +5,7 @@ import { Nav, INavLink, INavLinkGroup } from "office-ui-fabric-react/lib/Nav"
import { SourceGroup } from "../scripts/models/group"
import { SourceState, RSSSource } from "../scripts/models/source"
import { ALL } from "../scripts/models/feed"
import { AnimationClassNames, Stack, FocusZone, FocusZoneDirection } from "@fluentui/react"
import { AnimationClassNames, Stack } from "@fluentui/react"
export type MenuProps = {
status: boolean,

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { FeedContainer } from "../containers/feed-container"
import { AnimationClassNames, Icon } from "@fluentui/react"
import { AnimationClassNames, Icon, FocusTrapZone } from "@fluentui/react"
import ArticleContainer from "../containers/article-container"
import { ViewType } from "../scripts/models/page"
import ArticleSearch from "./utils/article-search"
@ -36,13 +36,17 @@ class Page extends React.Component<PageProps> {
))}
</div>}
{this.props.itemId && (
<div className="article-container" onClick={this.props.dismissItem}>
<FocusTrapZone
ignoreExternalFocusing={true}
isClickableOutsideFocusTrap={true}
className="article-container"
onClick={this.props.dismissItem}>
<div className={"article-wrapper " + AnimationClassNames.slideUpIn20} onClick={e => e.stopPropagation()}>
<ArticleContainer itemId={this.props.itemId} />
</div>
<div className="btn-group prev"><a className="btn" onClick={this.prevItem}><Icon iconName="Back" /></a></div>
<div className="btn-group next"><a className="btn" onClick={this.nextItem}><Icon iconName="Forward" /></a></div>
</div>
</FocusTrapZone>
)}
</>
)