LoginSignup
3
0

More than 5 years have passed since last update.

CSS に position: stickey という指定があります。指定要素がスクロール位置まで到達した場合、要素を画面位置固定する style です。この stickey ですが、ブラウザサポート状況がイマイチです。また、ピタっと固定されたタイミングを、javascript で捉える方法がありません。本日はこの振る舞いを実装しつつ、スクロール位置に応じて外部通知する手法を紹介します。
code: github / $ yarn 1209

1209.jpg 1209-1.jpg 1209-2.jpg

Context API / Provider 構成

本日も Context API を利用します。現在表示エリアに入っている Section の index を current に保持します。まずは型付き空 context を生成します。

components/context.ts
import { createContext, Dispatch, SetStateAction } from 'react'
export const CTX = createContext({} as {
  current: number
  setCurrent: Dispatch<SetStateAction<number>>
})

次に、useState の戻り値を Provider の初期値として注入します。先日と一緒です。

components/provider.tsx
export default (props: Props) => {
  const [current, setCurrent] = useState(0)
  return (
    <Provider value={{ current, setCurrent }}>
      {props.children}
    </Provider>
  )
}

App index で Provider を最高階に設置します。count はダミー作成用なので、気にしないでください。

components/index.tsx
const count = 5
export default () => (
  <Provider>
    <Sections count={count} />
    <Indicate count={count} />
  </Provider>
)

useStickyWrapper

Custom Hooks は今までのサンプルとほとんど変わりありません。画面内判定ロジックが多少異なり、今回は画面上部より上に section 上辺が位置する場合「画面内」と判定しています。

sections/section/useStickyWrapper.ts
const useStickyWrapper = (props: Props) => {
  const [state, setState] = useState<State>(...)
  const options = useMemo(...)
  useEffect(...) // 画面スクロール時処理
  useEffect(...) // 画面内判定処理
}

今回は wrapper コンポーネントと Custom Hooks を分離しています。

components/sections/section/stickyWrapper.tsx
export default (props: Props) => {
  const ref = useRef({} as HTMLDivElement)
  useStickyWrapper({
    ref,
    onEnter: props.onEnter,
    onLeave: props.onLeave,
    throttleInterval: props.throttleInterval
  })
  return (
    <div
      className={props.className}
      id={props.id}
      ref={ref}
    >
      {props.children}
    </div>
  )
}

Context の状態を変更する Section

ここまでで用意した StickyWrapper Component を利用し、Section を組み立てます。画面内外判定時の callback props は、親から注入されるハンドラをバインドします。

components/sections/section/index.tsx
const View = (props: Props) => (
  <StickyWrapper
    id={`section${props.index}`}
    className={props.className}
    onEnter={props.onEnter}
    onLeave={props.onLeave}
  >
    ...
  </StickyWrapper>
)

以下が親 Container です。「onEnter」「onLeave」 で Context の current を更新しているのが分かります。同時に、自身が画面内に入っているか否かのフラグを useState で確保し、子コンポーネントに注入します。

components/sections/section/index.tsx
export default (props: ContainerProps) => {
  const [isEnter, updateState] = useState(false)
  const { setCurrent } = useContext(CTX)
  const onEnter = useCallback(() => {
    updateState(true)
    setCurrent(props.index)
  }, [])
  const onLeave = useCallback(() => {
    updateState(false)
  }, [])
  return useMemo(
    () => (
      <StyledView
        index={props.index}
        title={props.title}
        onEnter={onEnter}
        onLeave={onLeave}
        isEnter={isEnter}
        isLast={props.isLast}
        isFirst={props.isFirst}
      />
    ),
    [isEnter]
  )
}

Indicate

画面右側に表示されているカレント表示です。Context の状態を参照します。

image.png

components/indicate/item.tsx
export default (props: ContainerProps) => {
  const { current } = useContext(CTX)
  const isCurrent = useMemo(() => current === props.index, [
    current
  ])
  const style = useMemo(
    () =>
      isCurrent
        ? {
            transform: 'scale(1.5)',
            backgroundColor: styles.blue
          }
        : {
            transform: 'scale(1)'
          },
    [isCurrent]
  )
  return useMemo(
    () => (
      <StyledView index={props.index} style={style} />
    ),
    [style]
  )
}

ここも memoize をサボらず施します。視覚効果としては地味ですが、アイデア次第で色々な機能に応用できそうです。

components/indicate/item.tsx
const StyledView = styled(View)`...`
components/indicate/item.tsx
const View = (props: Props) => (
  <span className={props.className} style={props.style} />
)
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0