Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: keyboard scrolling with j and k #117

Merged
merged 13 commits into from
Feb 18, 2019
18 changes: 9 additions & 9 deletions packages/components/src/components/cards/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { WikiPageListRow } from './partials/rows/WikiPageListRow'
export interface EventCardProps {
event: EnhancedGitHubEvent
repoIsKnown?: boolean
isFocused?: boolean
}

const styles = StyleSheet.create({
Expand All @@ -56,7 +57,7 @@ const styles = StyleSheet.create({
})

export const EventCard = React.memo((props: EventCardProps) => {
const { event, repoIsKnown } = props
const { event, repoIsKnown, isFocused } = props

const springAnimatedTheme = useSpringAnimatedTheme()

Expand Down Expand Up @@ -175,17 +176,16 @@ export const EventCard = React.memo((props: EventCardProps) => {

const smallLeftColumn = false

const getBackgroundColor = () => {
if (isFocused) return springAnimatedTheme.backgroundColorDarker2
if (isRead) return springAnimatedTheme.backgroundColorDarker1
return springAnimatedTheme.backgroundColor
}

return (
<SpringAnimatedView
key={`event-card-${id}-inner`}
style={[
styles.container,
{
backgroundColor: isRead
? springAnimatedTheme.backgroundColorDarker1
: springAnimatedTheme.backgroundColor,
},
]}
style={[styles.container, { backgroundColor: getBackgroundColor() }]}
>
<EventCardHeader
key={`event-card-header-${id}`}
Expand Down
36 changes: 32 additions & 4 deletions packages/components/src/components/cards/EventCards.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React from 'react'
import { View } from 'react-native'
import { FlatList, View } from 'react-native'

import { Column, constants, EnhancedGitHubEvent, LoadState } from '@devhub/core'
import { useKeyboardScrolling } from '../../hooks/use-keyboard-scrolling'
import { useReduxAction } from '../../hooks/use-redux-action'
import { useReduxState } from '../../hooks/use-redux-state'
import { ErrorBoundary } from '../../libs/bugsnag'
import * as actions from '../../redux/actions'
import { focusedColumnSelector } from '../../redux/selectors'
import { contentPadding } from '../../styles/variables'
import { Button } from '../common/Button'
import { FlatListWithOverlay } from '../common/FlatListWithOverlay'
Expand Down Expand Up @@ -36,6 +39,16 @@ export const EventCards = React.memo((props: EventCardsProps) => {
refresh,
} = props

const flatListRef = React.useRef<FlatList<View>>(null)

const focusedIndex = useKeyboardScrolling({
ref: flatListRef,
columnId: column.id,
length: events.length,
})

const focusedColumn = useReduxState(focusedColumnSelector)

const setColumnClearedAtFilter = useReduxAction(
actions.setColumnClearedAtFilter,
)
Expand Down Expand Up @@ -73,16 +86,30 @@ export const EventCards = React.memo((props: EventCardsProps) => {
return `event-card-${event.id}`
}

function renderItem({ item: event }: { item: EnhancedGitHubEvent }) {
function renderItem({
item: event,
index,
}: {
item: EnhancedGitHubEvent
index: number
}) {
if (props.swipeable) {
return (
<SwipeableEventCard event={event} repoIsKnown={props.repoIsKnown} />
<SwipeableEventCard
event={event}
repoIsKnown={props.repoIsKnown}
isFocused={columnIndex === focusedColumn && index === focusedIndex}
/>
)
}

return (
<ErrorBoundary>
<EventCard event={event} repoIsKnown={props.repoIsKnown} />
<EventCard
event={event}
repoIsKnown={props.repoIsKnown}
isFocused={columnIndex === focusedColumn && index === focusedIndex}
/>
</ErrorBoundary>
)
}
Expand Down Expand Up @@ -128,6 +155,7 @@ export const EventCards = React.memo((props: EventCardsProps) => {

return (
<FlatListWithOverlay
ref={flatListRef}
data={events}
ItemSeparatorComponent={CardItemSeparator}
ListFooterComponent={renderFooter}
Expand Down
18 changes: 9 additions & 9 deletions packages/components/src/components/cards/NotificationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface NotificationCardProps {
notification: EnhancedGitHubNotification
onlyOneRepository?: boolean
repoIsKnown?: boolean
isFocused?: boolean
}

const styles = StyleSheet.create({
Expand All @@ -45,7 +46,7 @@ const styles = StyleSheet.create({
})

export const NotificationCard = React.memo((props: NotificationCardProps) => {
const { notification, onlyOneRepository } = props
const { notification, onlyOneRepository, isFocused } = props

const springAnimatedTheme = useSpringAnimatedTheme()
const hasPrivateAccess = useReduxState(
Expand Down Expand Up @@ -171,17 +172,16 @@ export const NotificationCard = React.memo((props: NotificationCardProps) => {

const smallLeftColumn = false

const getBackgroundColor = () => {
if (isFocused) return springAnimatedTheme.backgroundColorDarker2
if (isRead) return springAnimatedTheme.backgroundColorDarker1
return springAnimatedTheme.backgroundColor
}

return (
<SpringAnimatedView
key={`notification-card-${id}-inner`}
style={[
styles.container,
{
backgroundColor: isRead
? springAnimatedTheme.backgroundColorDarker1
: springAnimatedTheme.backgroundColor,
},
]}
style={[styles.container, { backgroundColor: getBackgroundColor() }]}
>
<NotificationCardHeader
key={`notification-card-header-${id}`}
Expand Down
20 changes: 19 additions & 1 deletion packages/components/src/components/cards/NotificationCards.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react'
import { View } from 'react-native'
import { FlatList, View } from 'react-native'

import {
Column,
constants,
EnhancedGitHubNotification,
LoadState,
} from '@devhub/core'
import { useKeyboardScrolling } from '../../hooks/use-keyboard-scrolling'
import { useReduxAction } from '../../hooks/use-redux-action'
import { useReduxState } from '../../hooks/use-redux-state'
import { ErrorBoundary } from '../../libs/bugsnag'
import * as actions from '../../redux/actions'
import { focusedColumnSelector } from '../../redux/selectors'
import { contentPadding } from '../../styles/variables'
import { Button } from '../common/Button'
import { FlatListWithOverlay } from '../common/FlatListWithOverlay'
Expand Down Expand Up @@ -41,6 +44,16 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => {
refresh,
} = props

const flatListRef = React.useRef<FlatList<View>>(null)

const focusedIndex = useKeyboardScrolling({
ref: flatListRef,
columnId: column.id,
length: notifications.length,
})

const focusedColumn = useReduxState(focusedColumnSelector)

const setColumnClearedAtFilter = useReduxAction(
actions.setColumnClearedAtFilter,
)
Expand Down Expand Up @@ -79,14 +92,17 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => {

function renderItem({
item: notification,
index,
}: {
item: EnhancedGitHubNotification
index: number
}) {
if (props.swipeable) {
return (
<SwipeableNotificationCard
notification={notification}
repoIsKnown={props.repoIsKnown}
isFocused={columnIndex === focusedColumn && index === focusedIndex}
/>
)
}
Expand All @@ -96,6 +112,7 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => {
<NotificationCard
notification={notification}
repoIsKnown={props.repoIsKnown}
isFocused={columnIndex === focusedColumn && index === focusedIndex}
/>
</ErrorBoundary>
)
Expand Down Expand Up @@ -142,6 +159,7 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => {

return (
<FlatListWithOverlay
ref={flatListRef}
key="notification-cards-flat-list"
ItemSeparatorComponent={CardItemSeparator}
ListFooterComponent={renderFooter}
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/hooks/use-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { DependencyList, useEffect } from 'react'

import { emitter } from '../setup'

export type EmitterType = 'FOCUS_ON_COLUMN'
export type EmitterType =
| 'FOCUS_ON_COLUMN'
| 'SCROLL_DOWN_COLUMN'
| 'SCROLL_UP_COLUMN'

export function useEmitter(
key?: EmitterType,
Expand Down
50 changes: 50 additions & 0 deletions packages/components/src/hooks/use-keyboard-scrolling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import { FlatList, View } from 'react-native'
import { useEmitter } from './use-emitter'

interface KeyboardScrollingConfig {
ref: React.RefObject<FlatList<View>>
columnId: string
length: number
}

export function useKeyboardScrolling({
ref,
columnId,
length,
}: KeyboardScrollingConfig) {
const [selectedCardIndex, setSelectedCardIndex] = React.useState(0)
useEmitter(
'SCROLL_DOWN_COLUMN',
(payload: { columnId: string }) => {
if (!ref.current) return
if (columnId !== payload.columnId) return
const index = selectedCardIndex + 1
if (index < length) {
ref.current.scrollToIndex({
animated: true,
index,
})
setSelectedCardIndex(index)
}
},
[selectedCardIndex, length],
)
useEmitter(
'SCROLL_UP_COLUMN',
(payload: { columnId: string }) => {
if (!ref.current) return
if (columnId !== payload.columnId) return
const index = selectedCardIndex - 1
if (index >= 0) {
ref.current.scrollToIndex({
animated: true,
index,
})
setSelectedCardIndex(index)
}
},
[selectedCardIndex, length],
)
return selectedCardIndex
}
4 changes: 4 additions & 0 deletions packages/components/src/redux/actions/columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
} from '@devhub/core'
import { createAction } from '../helpers'

export function focusColumn(payload: number) {
return createAction('FOCUS_COLUMN', payload)
}

export function replaceColumnsAndSubscriptions(
payload: ColumnsAndSubscriptions,
) {
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/redux/reducers/columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@ export interface State {
allIds: string[]
byId: Record<string, Column | undefined> | null
updatedAt: string | null
focused: number
}

const initialState: State = {
allIds: [],
byId: null,
updatedAt: null,
focused: 0,
}

export const columnsReducer: Reducer<State> = (
state = initialState,
action,
) => {
switch (action.type) {
case 'FOCUS_COLUMN':
return immer(state, draft => {
draft.focused = action.payload
})
case 'ADD_COLUMN_AND_SUBSCRIPTIONS':
return immer(state, draft => {
draft.allIds = draft.allIds || []
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/redux/selectors/columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { createSubscriptionSelector } from './subscriptions'

const s = (state: RootState) => state.columns || {}

export const focusedColumnSelector = (state: RootState) => s(state).focused

export const createColumnSelector = () =>
createSelector(
(state: RootState) => s(state).byId,
Expand Down
Loading