Show column switcher on small screens

This commit is contained in:
Bruno Lemos
2019-04-07 01:29:50 -03:00
parent 72dfca5a3c
commit f76acc0b48
10 changed files with 272 additions and 41 deletions

View File

@@ -14,6 +14,10 @@ import {
import { SpringAnimatedActivityIndicator } from '../animated/spring/SpringAnimatedActivityIndicator'
import { SpringAnimatedText } from '../animated/spring/SpringAnimatedText'
import { Button, defaultButtonSize } from '../common/Button'
import { fabSize } from '../common/FAB'
import { Spacer } from '../common/Spacer'
import { useAppLayout } from '../context/LayoutContext'
import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer'
import { GenericMessageWithButtonView } from './GenericMessageWithButtonView'
const clearMessages = [
@@ -65,6 +69,7 @@ export const EmptyCards = React.memo((props: EmptyCardsProps) => {
refresh,
} = props
const { sizename } = useAppLayout()
const springAnimatedTheme = useCSSVariablesOrSpringAnimatedTheme()
const setColumnClearedAtFilter = useReduxAction(
actions.setColumnClearedAtFilter,
@@ -180,6 +185,10 @@ export const EmptyCards = React.memo((props: EmptyCardsProps) => {
</View>
) : null}
</View>
{shouldRenderFAB(sizename) && (
<Spacer height={fabSize + 2 * fabSpacing} />
)}
</View>
)
})

View File

@@ -17,8 +17,12 @@ import { bugsnag, ErrorBoundary } from '../../libs/bugsnag'
import * as actions from '../../redux/actions'
import { contentPadding } from '../../styles/variables'
import { Button } from '../common/Button'
import { fabSize } from '../common/FAB'
import { RefreshControl } from '../common/RefreshControl'
import { Spacer } from '../common/Spacer'
import { useFocusedColumn } from '../context/ColumnFocusContext'
import { useAppLayout } from '../context/LayoutContext'
import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer'
import { EmptyCards, EmptyCardsProps } from './EmptyCards'
import { EventCard } from './EventCard'
import { CardItemSeparator } from './partials/CardItemSeparator'
@@ -167,6 +171,8 @@ export const EventCards = React.memo((props: EventCardsProps) => {
])
const renderFooter = useCallback(() => {
const { sizename } = useAppLayout()
return (
<>
<CardItemSeparator />
@@ -201,6 +207,10 @@ export const EventCards = React.memo((props: EventCardsProps) => {
/>
</View>
) : null}
{shouldRenderFAB(sizename) && (
<Spacer height={fabSize + 2 * fabSpacing} />
)}
</>
)
}, [

View File

@@ -17,8 +17,12 @@ import { bugsnag, ErrorBoundary } from '../../libs/bugsnag'
import * as actions from '../../redux/actions'
import { contentPadding } from '../../styles/variables'
import { Button } from '../common/Button'
import { fabSize } from '../common/FAB'
import { RefreshControl } from '../common/RefreshControl'
import { Spacer } from '../common/Spacer'
import { useFocusedColumn } from '../context/ColumnFocusContext'
import { useAppLayout } from '../context/LayoutContext'
import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer'
import { EmptyCards, EmptyCardsProps } from './EmptyCards'
import { NotificationCard } from './NotificationCard'
import { CardItemSeparator } from './partials/CardItemSeparator'
@@ -170,6 +174,8 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => {
])
const renderFooter = useCallback(() => {
const { sizename } = useAppLayout()
return (
<>
<CardItemSeparator />
@@ -204,6 +210,10 @@ export const NotificationCards = React.memo((props: NotificationCardsProps) => {
/>
</View>
) : null}
{shouldRenderFAB(sizename) && (
<Spacer height={fabSize + 2 * fabSpacing} />
)}
</>
)
}, [

View File

@@ -0,0 +1,139 @@
import React from 'react'
import { StyleSheet } from 'react-native'
import { useAppViewMode } from '../../hooks/use-app-view-mode'
import { useCSSVariablesOrSpringAnimatedTheme } from '../../hooks/use-css-variables-or-spring--animated-theme'
import { useReduxState } from '../../hooks/use-redux-state'
import { emitter } from '../../libs/emitter'
import * as selectors from '../../redux/selectors'
import { contentPadding } from '../../styles/variables'
import { SpringAnimatedIcon } from '../animated/spring/SpringAnimatedIcon'
import { SpringAnimatedTouchableOpacity } from '../animated/spring/SpringAnimatedTouchableOpacity'
import { SpringAnimatedView } from '../animated/spring/SpringAnimatedView'
import { fabSize } from '../common/FAB'
import { useFocusedColumn } from '../context/ColumnFocusContext'
import { useAppLayout } from '../context/LayoutContext'
import { fabSpacing, shouldRenderFAB } from '../layout/FABRenderer'
const spacing = fabSpacing
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: spacing,
left: contentPadding,
flexDirection: 'row',
borderRadius: fabSize / 2,
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 3,
},
shadowOpacity: 0.2,
shadowRadius: 6,
overflow: 'hidden',
zIndex: 1000,
},
})
export function ColumnSwitcher() {
const columnIds = useReduxState(selectors.columnIdsSelector)
const currentOpenedModal = useReduxState(selectors.currentOpenedModal)
const focusedColumnId = useFocusedColumn() || columnIds[0]
const springAnimatedTheme = useCSSVariablesOrSpringAnimatedTheme()
const { appViewMode } = useAppViewMode()
const { sizename } = useAppLayout()
if (!(appViewMode === 'single-column' && shouldRenderFAB(sizename)))
return null
if (currentOpenedModal) return null
const isFirst = focusedColumnId === columnIds[0]
const isLast = focusedColumnId === columnIds.slice(-1)[0]
return (
<SpringAnimatedView
style={[
styles.container,
{
backgroundColor: springAnimatedTheme.backgroundColorMore1,
},
]}
>
<SpringAnimatedTouchableOpacity
analyticsCategory="switch-column-previous"
hitSlop={{
top: contentPadding / 2,
bottom: contentPadding / 2,
left: contentPadding,
}}
onPress={() => {
emitter.emit('FOCUS_ON_PREVIOUS_COLUMN', {
highlight: isFirst,
})
}}
style={[
{
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: fabSize,
height: fabSize,
borderRadius: 0,
borderTopLeftRadius: fabSize / 2,
borderBottomLeftRadius: fabSize / 2,
backgroundColor: springAnimatedTheme.backgroundColor,
},
isFirst && { opacity: 0.5 },
]}
>
<SpringAnimatedIcon
name="chevron-left"
size={14}
style={{
lineHeight: fabSize / 2.5,
fontSize: fabSize / 2.5,
color: springAnimatedTheme.foregroundColor,
}}
/>
</SpringAnimatedTouchableOpacity>
<SpringAnimatedTouchableOpacity
analyticsCategory="switch-column-next"
hitSlop={{
top: contentPadding / 2,
bottom: contentPadding / 2,
right: contentPadding,
}}
onPress={() => {
emitter.emit('FOCUS_ON_NEXT_COLUMN', {
highlight: isLast,
})
}}
style={[
{
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
width: fabSize,
height: fabSize,
borderRadius: 0,
borderTopRightRadius: fabSize / 2,
borderBottomRightRadius: fabSize / 2,
backgroundColor: springAnimatedTheme.backgroundColor,
},
isLast && { opacity: 0.5 },
]}
>
<SpringAnimatedIcon
name="chevron-right"
size={14}
style={{
lineHeight: fabSize / 2.5,
fontSize: fabSize / 2.5,
color: springAnimatedTheme.foregroundColor,
}}
/>
</SpringAnimatedTouchableOpacity>
</SpringAnimatedView>
)
}

View File

@@ -13,7 +13,7 @@ import {
} from '../animated/spring/SpringAnimatedTouchableOpacity'
import { SpringAnimatedView } from '../animated/spring/SpringAnimatedView'
export const fabSize = 50
export const fabSize = 44
export interface FABProps extends SpringAnimatedTouchableOpacityProps {
children?: string | React.ReactElement<any>
@@ -98,11 +98,11 @@ export function FAB(props: FABProps) {
name={iconName}
style={[
{
width: 24,
height: 24,
lineHeight: 24,
width: fabSize / 2,
height: fabSize / 2,
lineHeight: fabSize / 2,
marginTop: 1,
fontSize: 24,
fontSize: fabSize / 2,
textAlign: 'center',
color: useBrandColor
? springAnimatedTheme.primaryForegroundColor

View File

@@ -1,6 +1,9 @@
import React, { useContext, useState } from 'react'
import { useEmitter } from '../../hooks/use-emitter'
import { emitter } from '../../libs/emitter'
import { useReduxStore } from '../../redux/context/ReduxStoreContext'
import * as selectors from '../../redux/selectors'
export interface ColumnFocusProviderProps {
children?: React.ReactNode
@@ -13,6 +16,7 @@ export const ColumnFocusContext = React.createContext<ColumnFocusProviderState>(
)
export function ColumnFocusProvider(props: ColumnFocusProviderProps) {
const store = useReduxStore()
const [columnId, setColumnId] = useState<ColumnFocusProviderState>(null)
useEmitter(
@@ -23,6 +27,52 @@ export function ColumnFocusProvider(props: ColumnFocusProviderProps) {
[],
)
useEmitter(
'FOCUS_ON_PREVIOUS_COLUMN',
payload => {
const state = store.getState()
const columnIds = selectors.columnIdsSelector(state)
const focusedColumnIndex = columnIds
? columnIds.findIndex(id => id === (columnId || columnIds[0]))
: -1
const previousColumnIndex = Math.max(
0,
Math.min(focusedColumnIndex - 1, columnIds.length - 1),
)
emitter.emit('FOCUS_ON_COLUMN', {
...payload,
columnId: columnIds[previousColumnIndex],
columnIndex: previousColumnIndex,
})
},
[columnId],
)
useEmitter(
'FOCUS_ON_NEXT_COLUMN',
payload => {
const state = store.getState()
const columnIds = selectors.columnIdsSelector(state)
const focusedColumnIndex = columnIds
? columnIds.findIndex(id => id === (columnId || columnIds[0]))
: -1
const nextColumnIndex = Math.max(
0,
Math.min(focusedColumnIndex + 1, columnIds.length - 1),
)
emitter.emit('FOCUS_ON_COLUMN', {
...payload,
columnId: columnIds[nextColumnIndex],
columnIndex: nextColumnIndex,
})
},
[columnId],
)
useEmitter(
'SCROLL_DOWN_COLUMN',
payload => {

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { View, ViewStyle } from 'react-native'
import { StyleSheet, View } from 'react-native'
import { useKeyboardVisibility } from '../../hooks/use-keyboard-visibility'
import { useReduxAction } from '../../hooks/use-redux-action'
@@ -7,18 +7,30 @@ import { useReduxState } from '../../hooks/use-redux-state'
import * as actions from '../../redux/actions'
import * as selectors from '../../redux/selectors'
import { contentPadding } from '../../styles/variables'
import { defaultButtonSize } from '../common/Button'
import { FAB, fabSize } from '../common/FAB'
import { useAppLayout } from '../context/LayoutContext'
import { FAB } from '../common/FAB'
import { AppLayoutProviderState, useAppLayout } from '../context/LayoutContext'
export const fabSpacing =
contentPadding / 2 + Math.max(0, (fabSize - defaultButtonSize) / 2) - 2
export const fabSpacing = contentPadding / 2 // + Math.max(0, (fabSize - defaultButtonSize) / 2) - 2
const fabPositionStyle: ViewStyle = {
position: 'absolute',
bottom: fabSpacing,
right: contentPadding,
zIndex: 1000,
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: fabSpacing,
right: contentPadding,
zIndex: 1000,
},
})
export function shouldRenderFAB(
sizename: AppLayoutProviderState['sizename'],
keyboardVisibility?: ReturnType<typeof useKeyboardVisibility>,
) {
if (!(sizename <= '3-large')) return false
if (keyboardVisibility === 'appearing' || keyboardVisibility === 'visible')
return false
return true
}
export function FABRenderer() {
@@ -32,9 +44,7 @@ export function FABRenderer() {
const closeAllModals = useReduxAction(actions.closeAllModals)
const replaceModal = useReduxAction(actions.replaceModal)
if (!(sizename < '3-large')) return null
if (keyboardVisibility === 'appearing' || keyboardVisibility === 'visible')
return null
if (!shouldRenderFAB(sizename, keyboardVisibility)) return null
if (!currentOpenedModal) {
/*
@@ -57,7 +67,7 @@ export function FABRenderer() {
const iconStyle = undefined
return (
<View collapsable={false} style={fabPositionStyle}>
<View collapsable={false} style={styles.container}>
<FAB
key="fab"
analyticsLabel="add_column"
@@ -93,7 +103,7 @@ export function FABRenderer() {
const iconStyle = undefined
return (
<View style={fabPositionStyle}>
<View style={styles.container}>
<FAB
analyticsLabel="close_modals"
key="fab"

View File

@@ -35,6 +35,7 @@ import { Spacer } from '../common/Spacer'
import { useFocusedColumn } from '../context/ColumnFocusContext'
import { useAppLayout } from '../context/LayoutContext'
import { useTheme } from '../context/ThemeContext'
import { shouldRenderFAB } from './FABRenderer'
const logo = require('@devhub/components/assets/logo_circle.png') // tslint:disable-line
@@ -189,7 +190,9 @@ export const Sidebar = React.memo((props: SidebarProps) => {
<FlatList
ref={flatListRef}
ListHeaderComponent={
!(columnIds && columnIds.length) && !large ? (
!(columnIds && columnIds.length) &&
!large &&
!shouldRenderFAB(sizename) ? (
<>
<ColumnHeaderItem
analyticsLabel="sidebar_add"
@@ -283,7 +286,7 @@ export const Sidebar = React.memo((props: SidebarProps) => {
<>
{/* <Separator horizontal={!horizontal} /> */}
{!!large && (
{!!large && !shouldRenderFAB(sizename) && (
<>
<ColumnHeaderItem
analyticsLabel="sidebar_add"

View File

@@ -12,6 +12,18 @@ export interface EmitterTypes {
highlight?: boolean
scrollTo?: boolean
}
FOCUS_ON_PREVIOUS_COLUMN: {
animated?: boolean
focusOnVisibleItem?: boolean
highlight?: boolean
scrollTo?: boolean
}
FOCUS_ON_NEXT_COLUMN: {
animated?: boolean
focusOnVisibleItem?: boolean
highlight?: boolean
scrollTo?: boolean
}
SCROLL_DOWN_COLUMN: { columnId: string }
SCROLL_UP_COLUMN: { columnId: string }
}

View File

@@ -6,6 +6,7 @@ import url from 'url'
import { ColumnSeparator } from '../components/columns/ColumnSeparator'
import { ColumnsRenderer } from '../components/columns/ColumnsRenderer'
import { ColumnSwitcher } from '../components/columns/ColumnSwitcher'
import { ConditionalWrap } from '../components/common/ConditionalWrap'
import { Screen } from '../components/common/Screen'
import { Separator } from '../components/common/Separator'
@@ -188,20 +189,13 @@ export const MainScreen = React.memo(() => {
const scrollLeft = useCallback(() => {
if (currentOpenedModal) return
const previousColumnIndex = Math.max(
0,
Math.min(focusedColumnIndex - 1, columnIds.length - 1),
)
emitter.emit('FOCUS_ON_COLUMN', {
emitter.emit('FOCUS_ON_PREVIOUS_COLUMN', {
animated: true,
columnId: columnIds[previousColumnIndex],
columnIndex: previousColumnIndex,
focusOnVisibleItem: true,
highlight: false,
scrollTo: true,
})
}, [currentOpenedModal, focusedColumnIndex, columnIds])
}, [currentOpenedModal])
useKeyPressCallback('ArrowLeft', scrollLeft)
useKeyPressCallback('h', scrollLeft)
@@ -209,20 +203,13 @@ export const MainScreen = React.memo(() => {
const scrollRight = useCallback(() => {
if (currentOpenedModal) return
const nextColumnIndex = Math.max(
0,
Math.min(focusedColumnIndex + 1, columnIds.length - 1),
)
emitter.emit('FOCUS_ON_COLUMN', {
emitter.emit('FOCUS_ON_NEXT_COLUMN', {
animated: true,
columnId: columnIds[nextColumnIndex],
columnIndex: nextColumnIndex,
focusOnVisibleItem: true,
highlight: false,
scrollTo: true,
})
}, [currentOpenedModal, focusedColumnIndex, columnIds])
}, [currentOpenedModal])
useKeyPressCallback('ArrowRight', scrollRight)
useKeyPressCallback('l', scrollRight)
@@ -367,6 +354,7 @@ export const MainScreen = React.memo(() => {
/>
<ColumnsRenderer key="columns-renderer" />
<ColumnSwitcher key="column-switcher-renderer" />
<FABRenderer key="fab-renderer" />
</ConditionalWrap>
</View>