diff --git a/README.md b/README.md
index 4efcc582a..2e2252c53 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@

## Demo
-아래 QR 코드를 카메라로 찍어보세요. 당근마켓 앱 내에서 데모를 볼 수 있어요.
+아래 QR 코드를 카메라로 찍어보세요. 당근마켓 앱 내에서 데모를 볼 수 있어요. 아래 데모에서는 각 플랫폼 (Android/iOS) 환경에 맞게 UI가 표현됩니다.

@@ -15,94 +15,150 @@ $ yarn add @daangn/karrotframe
import * as kf from '@daangn/karrotframe'
```
-## 네비게이터
-네비게이터는 화면간 전환 효과와 History를 관리합니다. 네비게이터의 핵심 로직은 `react-router-dom`과 `recoil`에 의존하고 있습니다.
+## 1. 네비게이터
+네비게이터는 아래의 기능을 지원합니다.
-- 자연스러운 화면전환
- History 지원
-- 네비게이션 바
-- 이전, 닫기 버튼
+- 각 플랫폼에 맞게 디자인된 자연스러운 화면전환
+- 각 플랫폼에 맞게 디자인된 네비게이션 바
+- 상황에 맞는 이전, 닫기 버튼
+
+> 네비게이터의 핵심 로직은 `react-router-dom`과 `recoil`에 의존하고 있습니다.
+
+### 1-a. `Navigator`
+`Navigator` 컴포넌트는 화면을 표현하는데 반드시 필요한 요소들이 포함됩니다. 컴포넌트 트리 상단에 포함해주세요
+
+```tsx
+import { Navigator } from '@daangn/karrotframe'
+
+const App: React.FC = () => {
+ return (
+ {
+ console.log('닫기버튼이 눌렸습니다')
+ }}
+ >
+ {/*...*/}
+
+ )
+}
+```
+
+| Props | 타입 | 역할 | 기본값 |
+| ------------- | ------------- | ------------- | ------------- |
+| `theme` | `Cupertino`, `Android`, `Web` | UI 테마 | `Web` |
+| `animationDuration` | number | 애니메이션 지속시간 | 테마별로 다름 |
+| `useCustomRecoilRoot` | boolean | `true`인 경우 `Navigator` 내에 포함된 ` ` 을 제거합니다 | `false` |
+| `useCustomRouter` | boolean | `true`인 경우 `Navigator` 내에 포함된 ` ` 을 제거합니다 | `false` |
+
+### 1-b. `Screen`
+`Screen` 컴포넌트는 화면을 선언하는데 사용합니다. `Navigator` 안에 선언합니다.
-### 시작하기
-네비게이터의 핵심 컴포넌트로는 `Navigator`와 `Screen`이 존재합니다. `Navigator`의 `environment` props로 iOS(`Cupertino`), Android, Web 환경의 UI/애니메이션을 다르게 설정할 수 있습니다.
```tsx
import { Navigator, Screen } from '@daangn/karrotframe'
const App: React.FC = () => {
return (
{
- window.alert('닫기')
+ console.log('닫기버튼이 눌렸습니다')
}}
>
+
+ {/* 또는 */}
-
-
-
-
-
-
-
+
)
}
```
+| Props | 타입 | 역할 | 기본값 |
+| ------------- | ------------- | ------------- | ------------- |
+| `path` | string | 해당 화면을 표현할 Path | required |
+| `component` | `React.ComponentType` | 렌더링 할 컴포넌트 | |
+| `children` | `React.ReactNode` | 렌더링 할 요소 | |
+> `component` 또는 `children`은 반드시 사용하세요 (만약 두 props가 동시에 선언되는 경우, `component`가 우선권을 갖습니다)
-### `ScreenHelmet`
-기본적으로 Screen은 상단 네비게이션 바를 포함하고 있지 않습니다. 기본 제공되는 상단 네비게이션 바를 수정하기 위해서는 `ScreenHelmet` 컴포넌트를 사용하세요.
+### 1-c. `ScreenHelmet`
+기본적으로 Screen은 상단 네비게이션 바를 포함하고 있지 않습니다. 기본 제공되는 상단 네비게이션 바를 추가, 수정하기 위해서는 `ScreenHelmet` 컴포넌트를 사용하세요.
```tsx
import { ScreenHelmet } from '@daangn/karrotframe'
-const Home: React.FC = () => {
+const MyComponent: React.FC = () => {
return (
왼쪽에추가
+ }
+ appendRight={
+ 오른쪽에추가
+ }
+ customBackButton={
+ 이전
+ }
+ customCloseButton={
+ 닫기
+ }
/>
)
}
```
+| Props | 타입 | 역할 | 기본값 |
+| ------------- | ------------- | ------------- | ------------- |
+| `title` | `React.ReactNode` | 타이틀 부분에 출력할 요소 | `undefined` |
+| `appendLeft` | `React.ReactNode` | 왼쪽에 요소를 추가 (이전 버튼 오른쪽에 표시됩니다) | `undefined` |
+| `appendRight` | `React.ReactNode` | 오른쪽에 요소를 추가 (닫기 버튼 왼쪽에 표시됩니다) | `undefined` |
+| `customBackButton` | `React.ReactNode` | 이전 버튼을 사용자화합니다 | `undefined` |
+| `customCloseButton` | `React.ReactNode` | 닫기 버튼을 사용자화합니다 | `undefined` |
-다음과 같이 좌측, 우측에 Element를 추가하고, 가운데 타이틀을 덮어씌울수 있습니다.
+### 1-d. `Link`
+특정 path로 이동할 수 있는 링크를 생성하는 컴포넌트입니다.
```tsx
-import { ScreenHelmet } from '@daangn/karrotframe'
+import { Link } from '@daangn/karrotframe'
const Home: React.FC = () => {
return (
- left
- }
- right={
- right
- }
- center={
- 홈
- }
- />
+ 글 목록
)
}
```
+| Props | 타입 | 역할 | 기본값 |
+| ------------- | ------------- | ------------- | ------------- |
+| `to` | string | 이동 할 path | required |
+| `replace` | boolean | path 이동을 replace로 처리할 지 여부 | `undefined` |
+| `className` | string | className | `undefined` |
-### URL 파라미터 받기
-`useLocation`, `useParams`, `useRouteMatch`를 활용할 수 있습니다
+### 1-e. `useLocation`, `useParams`, `useRouteMatch`
+react-router-dom에 존재하는 `useLocation`, `useParams`, `useRouteMatch`를 그대로 사용할 수 있습니다
```tsx
import { useLocation, useParams, useRouteMatch } from '@daangn/karrotframe'
const Post: React.FC = () => {
+ /**
+ * 현재 location 정보
+ */
const location = useLocation()
+ /**
+ * path parameter로 들어온 값
+ */
const params = useParams()
+ /**
+ * 현재 위치와 특정 path regex를 비교해 파싱된 값을 반환합니다.
+ * (매치하지 않는다면 null 반환)
+ */
const match = useRouteMatch({
path: '/:post_id',
})
@@ -111,64 +167,77 @@ const Post: React.FC = () => {
}
```
-### 화면 전환
-화면 전환은 `Link` 또는 `useNavigator` 를 통해 수행할 수 있습니다.
+### 1-f. `useNavigator`
+화면 전환을 수행합니다.
```tsx
-import { Link } from '@daangn/karrotframe'
+import { useNavigator } from '@daangn/karrotframe'
-const Home: React.FC = () => {
- return (
-
- 글 목록
-
- )
-}
-```
+const Posts: React.FC = () => {
+ const { push, pop, replace } = useNavigator()
-또는
+ const goPost = (postId: string) => () => {
+ // 특정 path로 이동합니다
+ push(`/posts/${postId}`)
+ }
-```tsx
-import { useNavigator } from '@daangn/karrotframe'
+ const goBack = () => {
+ // 한단계 뒤로 갑니다
+ pop()
+
+ // depth argument를 통해 여러단계를 pop 할 수 있습니다
+ pop(1)
+ }
+
+ useEffect(() => {
+ if (!user) {
+ // 특정 path로 이동합니다 (replace)
+ // 애니메이션 없이 이동하므로, redirect behavior에 적절합니다
+ replace('/login')
+ }
+ })
-const Home: React.FC = () => {
- const { push } = useNavigator()
return (
-
{
- push('/posts')
- }}
- >
- 글 목록
-
+ {posts.map((post) => {
+ return (
+
+ {/* ... */}
+
+ 글 상세보기
+
+
+ )
+ })}
+ {/* ... */}
+ 뒤로가기
)
}
```
-### 화면 간 데이터 전송
-`useNavigator`의 `pop()`과 `await push()`를 통해 화면간 데이터 전송을 할 수 있습니다.
+추가적으로, `useNavigator`의 `pop().send()`과 `await push()`를 통해 화면간 데이터 전송을 할 수 있습니다.
-`pop()` 함수 내 `depth` argument를 2 이상으로 부여할 시 여러 화면을 뛰어넘어서 전송도 가능합니다.
+> `pop()` 함수 내 `depth` argument를 2 이상으로 부여할 시 여러 화면을 뛰어넘어서 전송도 가능합니다.
```tsx
import { useNavigator } from '@daangn/karrotframe'
-const Home: React.FC = () => {
+const Posts: React.FC = () => {
const { push } = useNavigator()
+
+ const writePost = () => {
+ // 다음 화면에서 전송할 데이터를 기다립니다
+ const data = await push('/posts/write')
+ console.log(data)
+ // {
+ // hello: 'world',
+ // }
+ }
return (
{
- const data = await push('/form')
-
- console.log(data)
- // {
- // title: '안녕하세요',
- // content: '데이터 전송합니다.'
- // }
- }}
+ onClick={writePost}
>
글 작성하기
@@ -176,13 +245,18 @@ const Home: React.FC = () => {
)
}
-const Form: React.FC = () => {
+const PostWriteForm: React.FC = () => {
const { pop } = useNavigator()
const onSubmit = () => {
- pop(1, {
- title: '안녕하세요',
- content: '데이터 전송합니다.'
+ // 이전 화면으로 데이터 전송
+ pop().send({
+ hello: 'world',
+ })
+
+ // 아래와 같이 여러 단계를 뛰어넘어 데이터 전송도 가능합니다
+ pop(3).send({
+ hello: 'world',
})
}
@@ -190,9 +264,15 @@ const Form: React.FC = () => {
}
```
+## 2. 추가 예정인 기능
+- [ ] 하프뷰
+- [ ] 모달
+- [ ] 토스트
+> 추가적으로 Karrotframe에 추가되었으면하는 기능이 있으시다면, 꼭 이슈에 남겨주세요.
+
## Contributor
- Bucky (bucky@daangn.com)
- Tony (tony@daangn.com)
## Help
-슬랙의 #dev-frontend로 언제든지 편하게 문의해주세요!
+당근마켓 Slack 내 `#dev-frontend`로 언제든지 편하게 문의해주세요!
diff --git a/example/src/App.tsx b/example/src/App.tsx
index c55d8c4a4..315bc2a81 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -15,8 +15,7 @@ const bridge = new Bridge()
function App() {
return (
{
bridge.router.close()
}}>
diff --git a/example/src/components/Home.tsx b/example/src/components/Home.tsx
index fbfc65919..b0a329bcb 100644
--- a/example/src/components/Home.tsx
+++ b/example/src/components/Home.tsx
@@ -5,15 +5,16 @@ import styled from '@emotion/styled'
const Home: React.FC = () => {
const navigator = useNavigator()
- const onPage2Click = () => {
- navigator.push('/page2')
+ const onPage2Click = async () => {
+ const data = await navigator.push('/page2')
+ window.alert(JSON.stringify(data))
}
return (
@@ -22,7 +23,7 @@ const Home: React.FC = () => {
}
- right={
+ appendRight={
diff --git a/example/src/components/Page2.tsx b/example/src/components/Page2.tsx
index 04ad9ebcb..97ccc76a1 100644
--- a/example/src/components/Page2.tsx
+++ b/example/src/components/Page2.tsx
@@ -6,7 +6,9 @@ const Page2: React.FC = () => {
const navigator = useNavigator()
const onPopClick = () => {
- navigator.pop(1)
+ navigator.pop().send({
+ hello: 'world',
+ })
}
const onPage3Click = () => {
navigator.push('/page3')
diff --git a/src/navigator/Link.tsx b/src/navigator/Link.tsx
index 99792e10d..918edc5d5 100644
--- a/src/navigator/Link.tsx
+++ b/src/navigator/Link.tsx
@@ -1,22 +1,35 @@
-import qs from 'qs'
import React, { forwardRef, useMemo } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import short from 'short-uuid'
+import { appendSearch } from '../utils/appendSearch'
+
interface LinkProps {
+ /**
+ * 이동할 path
+ */
to: string
+
+ /**
+ * path 이동을 replace로 처리할 지 여부
+ */
replace?: boolean
+
+ /**
+ * className
+ */
className?: string
}
const Link: React.FC = forwardRef((props, ref) => {
const { pathname, search } = useMemo(() => {
- const sid = () => short.generate().substr(0, 5)
-
const [pathname, search] = props.to.split('?')
- const parsedSearch = search ? qs.parse(search) : null
+ const kf_sid = short.generate().substr(0, 5)
+
return {
pathname,
- search: qs.stringify({ ...parsedSearch, kf_sid: sid }),
+ search: appendSearch(search || null, {
+ kf_sid,
+ }),
}
}, [props.to])
diff --git a/src/navigator/Navigator.tsx b/src/navigator/Navigator.tsx
index 0d238cbc9..dddba34bd 100644
--- a/src/navigator/Navigator.tsx
+++ b/src/navigator/Navigator.tsx
@@ -14,9 +14,11 @@ import {
} from './atoms'
import { Card } from './components'
import { NavigatorOptionsProvider, useNavigatorOptions } from './contexts'
-import { Environment } from '../types'
+import { NavigatorTheme } from '../types'
-const DEFAULT_ANIMATION_DURATION = 350
+const DEFAULT_CUPERTINO_ANIMATION_DURATION = 350
+const DEFAULT_WEB_ANIMATION_DURATION = 270
+const DEFAULT_ANDROID_ANIMATION_DURATION = 270
/**
* Navigator가 이미 초기화되었는지 확인
@@ -26,37 +28,63 @@ let isNavigatorInitialized = false
interface NavigatorProps {
/**
- * 환경
+ * 테마 (기본값: Web)
*/
- environment: Environment
+ theme?: NavigatorTheme
/**
* 애니메이션 지속시간
*/
animationDuration?: number
+ /**
+ * 빌트인 된 RecoilRoot를 없애고, 사용자가 직접 RecoilRoot를 셋팅합니다
+ */
+ useCustomRecoilRoot?: boolean
+
+ /**
+ * 빌트인 된 react-router-dom의 HashRouter를 없애고, 사용자가 직접 Router를 셋팅합니다
+ */
+ useCustomRouter?: boolean
+
/**
* 닫기 버튼을 눌렀을때 해당 콜백이 호출됩니다
*/
onClose?: () => void
}
const Navigator: React.FC = (props) => {
- return (
-
-
-
- {props.children}
-
-
-
+ let h = (
+ {
+ switch (props.theme ?? 'Web') {
+ case 'Cupertino':
+ return DEFAULT_CUPERTINO_ANIMATION_DURATION
+ case 'Android':
+ return DEFAULT_ANDROID_ANIMATION_DURATION
+ case 'Web':
+ return DEFAULT_WEB_ANIMATION_DURATION
+ }
+ })(),
+ }}>
+ {props.children}
+
)
+
+ if (!props.useCustomRecoilRoot) {
+ h = {h}
+ }
+ if (!props.useCustomRouter) {
+ h = {h}
+ }
+
+ return h
}
-const NavigatorScreens: React.FC> = (props) => {
+const NavigatorScreens: React.FC = (props) => {
const location = useLocation()
const history = useHistory()
diff --git a/src/navigator/Screen.tsx b/src/navigator/Screen.tsx
index 40f5b7bcd..4967a55ca 100644
--- a/src/navigator/Screen.tsx
+++ b/src/navigator/Screen.tsx
@@ -11,7 +11,9 @@ interface ScreenProps {
*/
path: string
- children?: React.ReactNode
+ /**
+ * 해당 스크린에 표시할 컴포넌트
+ */
component?: React.ComponentType
}
const Screen: React.FC = (props) => {
diff --git a/src/navigator/ScreenHelmet.tsx b/src/navigator/ScreenHelmet.tsx
index f19de59b4..46bdf8938 100644
--- a/src/navigator/ScreenHelmet.tsx
+++ b/src/navigator/ScreenHelmet.tsx
@@ -3,10 +3,32 @@ import React, { useEffect } from 'react'
import { useScreenInstanceOptions } from './contexts'
interface ScreenHelmetProps {
- title?: string
- left?: React.ReactNode
- right?: React.ReactNode
- center?: React.ReactNode
+ /**
+ * 네비게이션의 타이틀
+ */
+ title?: React.ReactNode
+
+ /**
+ * 네비게이션의 왼쪽에 요소를 추가
+ * (이전 버튼 오른쪽에 표시됩니다)
+ */
+ appendLeft?: React.ReactNode
+
+ /**
+ * 네비게이션의 오른쪽 요소를 추가
+ * (닫기 버튼 왼쪽에 표시됩니다)
+ */
+ appendRight?: React.ReactNode
+
+ /**
+ * 이전 버튼을 사용자화합니다
+ */
+ customBackButton?: React.ReactNode
+
+ /**
+ * 닫기 버튼을 사용자화합니다
+ */
+ customCloseButton?: React.ReactNode
}
const ScreenHelmet: React.FC = (props) => {
const screen = useScreenInstanceOptions()
@@ -14,10 +36,11 @@ const ScreenHelmet: React.FC = (props) => {
useEffect(() => {
screen.setNavbar({
visible: true,
- title: props.title ?? '',
- left: props.left ?? null,
- right: props.right ?? null,
- center: props.center ?? null,
+ title: props.title ?? null,
+ appendLeft: props.appendLeft ?? null,
+ appendRight: props.appendRight ?? null,
+ customBackButton: props.customBackButton ?? null,
+ customCloseButton: props.customCloseButton ?? null,
})
}, [props])
diff --git a/src/navigator/atoms/ScreenEdge.ts b/src/navigator/atoms/ScreenEdge.ts
index 165f1313a..7f10471cd 100644
--- a/src/navigator/atoms/ScreenEdge.ts
+++ b/src/navigator/atoms/ScreenEdge.ts
@@ -4,7 +4,7 @@ export const AtomScreenEdge = atom<{
startX: number | null
startTime: number | null
}>({
- key: 'ScreenEdge',
+ key: 'KFScreenEdge',
default: {
startX: null,
startTime: null,
diff --git a/src/navigator/atoms/ScreenInstanceOptions.ts b/src/navigator/atoms/ScreenInstanceOptions.ts
index c9c6d8cae..007b3cc25 100644
--- a/src/navigator/atoms/ScreenInstanceOptions.ts
+++ b/src/navigator/atoms/ScreenInstanceOptions.ts
@@ -2,11 +2,12 @@ import React from 'react'
import { atom } from 'recoil'
export interface NavbarOptions {
- title: string
visible: boolean
- left: React.ReactNode | null
- right: React.ReactNode | null
- center: React.ReactNode | null
+ title: React.ReactNode | null
+ appendLeft: React.ReactNode | null
+ appendRight: React.ReactNode | null
+ customBackButton: React.ReactNode | null
+ customCloseButton: React.ReactNode | null
}
export const AtomScreenInstanceOptions = atom<{
@@ -14,6 +15,6 @@ export const AtomScreenInstanceOptions = atom<{
navbar: NavbarOptions
}
}>({
- key: 'ScreenInstanceOptions',
+ key: 'KFScreenInstanceOptions',
default: {},
})
diff --git a/src/navigator/atoms/ScreenInstancePointer.ts b/src/navigator/atoms/ScreenInstancePointer.ts
index 88e4395d9..65924143b 100644
--- a/src/navigator/atoms/ScreenInstancePointer.ts
+++ b/src/navigator/atoms/ScreenInstancePointer.ts
@@ -1,6 +1,6 @@
import { atom } from 'recoil'
export const AtomScreenInstancePointer = atom({
- key: 'ScreenInstancePointer',
+ key: 'KFScreenInstancePointer',
default: -1,
})
diff --git a/src/navigator/atoms/ScreenInstances.ts b/src/navigator/atoms/ScreenInstances.ts
index 3523a3cfd..69600fd9a 100644
--- a/src/navigator/atoms/ScreenInstances.ts
+++ b/src/navigator/atoms/ScreenInstances.ts
@@ -5,6 +5,6 @@ export interface ScreenInstance {
screenId: string
}
export const AtomScreenInstances = atom({
- key: 'ScreenInstances',
+ key: 'KFScreenInstances',
default: [],
})
diff --git a/src/navigator/atoms/Screens.ts b/src/navigator/atoms/Screens.ts
index 35370d9c3..da5d22844 100644
--- a/src/navigator/atoms/Screens.ts
+++ b/src/navigator/atoms/Screens.ts
@@ -13,6 +13,6 @@ export interface Screen {
export const AtomScreens = atom<{
[id: string]: Screen
}>({
- key: 'Screens',
+ key: 'KFScreens',
default: {},
})
diff --git a/src/navigator/components/Card.tsx b/src/navigator/components/Card.tsx
index acaf87b45..89fe8b4bd 100644
--- a/src/navigator/components/Card.tsx
+++ b/src/navigator/components/Card.tsx
@@ -6,7 +6,7 @@ import styled from '@emotion/styled'
import Navbar from './Navbar'
import { useNavigatorOptions } from '../contexts'
-import { Environment } from '../../types'
+import { NavigatorTheme } from '../../types'
import { AtomScreenInstanceOptions, AtomScreenEdge } from '../atoms'
interface CardProps {
@@ -38,7 +38,7 @@ const Card: React.FC = (props) => {
const $frameContainer = useRef(null)
const $hiddenDims = useMemo(
// eslint-disable-next-line
- () => document.getElementsByClassName('kf-dim_hidden') as HTMLCollectionOf,
+ () => document.getElementsByClassName('css-kf-card-dim_hidden') as HTMLCollectionOf,
[]
)
@@ -142,8 +142,12 @@ const Card: React.FC = (props) => {
return (
= (props) => {
{!!screenInstanceOption?.navbar.visible && (
)}
{props.children}
- {!props.isRoot && (
+ {navigatorOptions.theme === 'Cupertino' && !props.isRoot && (
)}
@@ -178,40 +184,50 @@ const Card: React.FC = (props) => {
}
interface DimProps {
+ navigatorTheme: NavigatorTheme
isTop: boolean
animationDuration: number
}
const Dim = styled.div`
width: 100%;
height: 100%;
- background-color: rgba(0, 0, 0, 0);
- transform: translateX(-5rem);
- transition: background-color ${(props) => props.animationDuration}ms,
- transform ${(props) => props.animationDuration}ms;
${(props) =>
- props.isTop &&
+ props.navigatorTheme === 'Cupertino' &&
css`
- transform: translateX(0);
+ background-color: rgba(0, 0, 0, 0);
+ transition: background-color ${props.animationDuration}ms, transform ${props.animationDuration}ms;
+ will-change: background-color, transform;
+
+ ${!props.isTop &&
+ css`
+ transform: translateX(-5rem);
+ `}
`}
`
interface FrameContainerProps {
+ navigatorTheme: NavigatorTheme
isRoot: boolean
animationDuration: number
}
const FrameContainer = styled.div`
width: 100%;
height: 100%;
- transition: transform ${(props) => props.animationDuration}ms;
- transform: translateX(0);
overflow-y: scroll;
background-color: #fff;
${(props) =>
- !props.isRoot &&
+ props.navigatorTheme === 'Cupertino' &&
css`
- transform: translateX(100%);
+ transform: translateX(0);
+ transition: transform ${props.animationDuration}ms;
+ will-change: transform;
+
+ ${!props.isRoot &&
+ css`
+ transform: translateX(100%);
+ `}
`};
`
@@ -230,7 +246,10 @@ const Edge = styled.div`
interface ContainerProps {
navbarVisible?: boolean
- environment: Environment
+ navigatorTheme: NavigatorTheme
+ animationDuration: number
+ isRoot: boolean
+ isTop: boolean
enterActive: boolean
enterDone: boolean
exitActive: boolean
@@ -250,7 +269,7 @@ const Container = styled.div`
return null
}
- switch (props.environment) {
+ switch (props.navigatorTheme) {
case 'Cupertino':
return css`
padding-top: 2.75rem;
@@ -264,29 +283,64 @@ const Container = styled.div`
}}
${(props) =>
- (props.enterActive || props.enterDone) &&
+ props.navigatorTheme === 'Cupertino' &&
css`
- .kf-dim {
- background-color: rgba(0, 0, 0, 0.2);
- }
- .kf-frame-container {
- transform: translateX(0);
- }
+ ${(props.enterActive || props.enterDone) &&
+ css`
+ .css-kf-card-dim {
+ background-color: rgba(0, 0, 0, 0.2);
+ }
+ .css-card-kf-frame-container {
+ transform: translateX(0);
+ }
+ `}
+ ${(props.exitActive || props.exitDone) &&
+ css`
+ .css-kf-card-dim {
+ background-color: rgba(0, 0, 0, 0);
+ transform: translateX(0);
+ }
+ .css-card-kf-frame-container {
+ transform: translateX(100%);
+ }
+ .css-kf-navbar-container {
+ display: none;
+ }
+ `}
`}
${(props) =>
- (props.exitActive || props.exitDone) &&
+ (props.navigatorTheme === 'Android' || props.navigatorTheme === 'Web') &&
css`
- .kf-dim {
- background-color: rgba(0, 0, 0, 0);
- transform: translateX(0);
- }
- .kf-frame-container {
- transform: translateX(100%);
- }
- .kf-navbar-container {
- display: none;
- }
+ opacity: 0;
+ transform: translateY(10rem);
+ transition: transform ${props.animationDuration}ms, opacity ${props.animationDuration}ms;
+ transition-timing-function: cubic-bezier(0.22, 0.67, 0.39, 0.83);
+ will-change: transform, opacity;
+
+ ${props.isRoot &&
+ css`
+ opacity: 1;
+ transform: translateY(0);
+ `}
+
+ ${(props.enterActive || props.enterDone) &&
+ css`
+ opacity: 1;
+ transform: translateY(0);
+ `}
+
+ ${!props.isTop &&
+ css`
+ transform: translateY(-2rem);
+ transition-timing-function: cubic-bezier(0.29, 0.55, 0.36, 0.69);
+ `}
+
+ ${(props.exitActive || props.exitActive) &&
+ css`
+ opacity: 0;
+ transform: translateY(10rem);
+ `}
`}
${(props) =>
diff --git a/src/navigator/components/Navbar.tsx b/src/navigator/components/Navbar.tsx
index aff0f62f5..09581e269 100644
--- a/src/navigator/components/Navbar.tsx
+++ b/src/navigator/components/Navbar.tsx
@@ -7,11 +7,11 @@ import styled from '@emotion/styled'
import { IconBack, IconClose } from '../assets'
import { AtomScreenInstanceOptions } from '../atoms'
import { useNavigatorOptions } from '../contexts'
-import { Environment } from '../../types'
+import { NavigatorTheme } from '../../types'
interface NavbarProps {
screenInstanceId: string
- environment: Environment
+ theme: NavigatorTheme
isRoot: boolean
onClose: () => void
}
@@ -25,38 +25,40 @@ const Navbar: React.FC = (props) => {
return (
- {props.environment === 'Cupertino' && (
-
- {screenInstanceOption?.navbar.center || screenInstanceOption?.navbar.title}
-
+ {props.theme === 'Cupertino' && (
+ {screenInstanceOption?.navbar.title}
)}
- {(!props.isRoot || screenInstanceOption?.navbar.left) && (
+ {(!props.isRoot || screenInstanceOption?.navbar.appendLeft) && (
- {!props.isRoot && (
-
-
-
- )}
- {screenInstanceOption?.navbar.left}
+ {!props.isRoot &&
+ (screenInstanceOption?.navbar.customBackButton ? (
+ {screenInstanceOption.navbar.customBackButton}
+ ) : (
+
+
+
+ ))}
+ {screenInstanceOption?.navbar.appendLeft}
)}
- {(props.environment === 'Android' || props.environment === 'Web') && (
-
- {screenInstanceOption?.navbar.center || screenInstanceOption?.navbar.title}
-
+ {(props.theme === 'Android' || props.theme === 'Web') && (
+ {screenInstanceOption?.navbar.title}
)}
- {(props.isRoot || screenInstanceOption?.navbar.right) && (
+ {(props.isRoot || screenInstanceOption?.navbar.appendRight) && (
- {screenInstanceOption?.navbar.right}
- {props.isRoot && (
-
-
-
- )}
+ {screenInstanceOption?.navbar.appendRight}
+ {props.isRoot &&
+ (screenInstanceOption.navbar.customCloseButton ? (
+ {screenInstanceOption.navbar.customCloseButton}
+ ) : (
+
+
+
+ ))}
)}
@@ -64,7 +66,11 @@ const Navbar: React.FC = (props) => {
)
}
-export const Container = styled.div<{ environment: Environment; animationDuration: number }>`
+interface ContainerProps {
+ navigatorTheme: NavigatorTheme
+ animationDuration: number
+}
+export const Container = styled.div`
background-color: #fff;
display: flex;
position: absolute;
@@ -72,7 +78,7 @@ export const Container = styled.div<{ environment: Environment; animationDuratio
top: 0;
${(props) => {
- switch (props.environment) {
+ switch (props.navigatorTheme) {
case 'Cupertino':
return css`
height: 2.75rem;
@@ -122,14 +128,14 @@ const Back = styled.div`
}
`
-const Center = styled.div<{ environment: Environment }>`
+const Center = styled.div<{ navigatorTheme: NavigatorTheme }>`
flex: 1;
display: flex;
align-items: center;
font-weight: bold;
${(props) => {
- switch (props.environment) {
+ switch (props.navigatorTheme) {
case 'Android':
case 'Web':
return css`
diff --git a/src/navigator/contexts/ContextNavigatorOptions.ts b/src/navigator/contexts/ContextNavigatorOptions.ts
index e3fec26b3..1ea36551a 100644
--- a/src/navigator/contexts/ContextNavigatorOptions.ts
+++ b/src/navigator/contexts/ContextNavigatorOptions.ts
@@ -1,9 +1,9 @@
import { createContext, useContext } from 'react'
-import { Environment } from '../../types'
+import { NavigatorTheme } from '../../types'
export const ContextNavigatorOptions = createContext<{
- environment: Environment
+ theme: NavigatorTheme
animationDuration: number
}>(null as any)
diff --git a/src/navigator/useNavigator.ts b/src/navigator/useNavigator.ts
index 9f9d52bee..16f46c3f3 100644
--- a/src/navigator/useNavigator.ts
+++ b/src/navigator/useNavigator.ts
@@ -1,8 +1,8 @@
-import qs from 'qs'
import { useHistory } from 'react-router-dom'
import { useRecoilState } from 'recoil'
import short from 'short-uuid'
+import { appendSearch } from '../utils/appendSearch'
import { AtomScreenInstances, AtomScreenInstancePointer, screenInstancePromises } from './atoms'
import { useScreenInstanceInfo } from './contexts'
@@ -16,40 +16,46 @@ export function useNavigator() {
return {
push(to: string) {
return new Promise((resolve) => {
- screenInstancePromises[screenInfo.screenInstanceId] = resolve
-
- const sid = short.generate().substr(0, 5)
-
const [pathname, search] = to.split('?')
- const parsedSearch = search ? qs.parse(search) : null
+ const kf_sid = short.generate().substr(0, 5)
+
+ history.push(
+ pathname +
+ '?' +
+ appendSearch(search || null, {
+ kf_sid,
+ })
+ )
- history.push(pathname + '?' + qs.stringify({ ...parsedSearch, kf_sid: sid }))
+ screenInstancePromises[screenInfo.screenInstanceId] = resolve
})
},
replace(to: string) {
- const sid = short.generate().substr(0, 5)
-
- const pathname = to.split('?')[0]
- let search = to.split('?')[1]
- const parsedSearch = search ? qs.parse(search) : null
-
- search = qs.stringify({
- ...parsedSearch,
- kf_sid: sid,
- })
-
- history.replace(pathname + '?' + search)
+ const [pathname, search] = to.split('?')
+ const kf_sid = short.generate().substr(0, 5)
+
+ history.replace(
+ pathname +
+ '?' +
+ appendSearch(search, {
+ kf_sid,
+ })
+ )
},
- pop(depth: number, data?: T) {
- const targetScreenInstance = screenInstances.find((_, index) => index === screenInstancePointer - depth)
-
- if (targetScreenInstance) {
- screenInstancePromises[targetScreenInstance.id]?.(data ?? null)
- }
-
+ pop(depth = 1) {
for (let i = 0; i < depth; i++) {
history.goBack()
}
+
+ return {
+ send(data: T) {
+ const targetScreenInstance = screenInstances.find((_, index) => index === screenInstancePointer - depth)
+
+ if (targetScreenInstance) {
+ screenInstancePromises[targetScreenInstance.id]?.(data ?? null)
+ }
+ },
+ }
},
}
}
diff --git a/src/types/index.ts b/src/types/index.ts
index 1feda75a4..c5021c9aa 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1 +1 @@
-export type Environment = 'Cupertino' | 'Android' | 'Web'
+export type NavigatorTheme = 'Cupertino' | 'Android' | 'Web'
diff --git a/src/utils/appendSearch.ts b/src/utils/appendSearch.ts
new file mode 100644
index 000000000..fc798477c
--- /dev/null
+++ b/src/utils/appendSearch.ts
@@ -0,0 +1,15 @@
+import qs from 'qs'
+
+export function appendSearch(
+ search: string | null,
+ dict: {
+ [key: string]: string
+ }
+) {
+ const parsedSearch = search ? qs.parse(search) : null
+
+ return qs.stringify({
+ ...parsedSearch,
+ ...dict,
+ })
+}