mirror of
https://github.com/zhigang1992/devhub.git
synced 2026-05-15 01:02:30 +08:00
Show column switcher on small screens
This commit is contained in:
@@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}, [
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}, [
|
||||
|
||||
139
packages/components/src/components/columns/ColumnSwitcher.tsx
Normal file
139
packages/components/src/components/columns/ColumnSwitcher.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user