Skip to content

Commit 820a896

Browse files
sputlit animations (#92)
#92
1 parent 3ab2f0b commit 820a896

File tree

3 files changed

+136
-27
lines changed

3 files changed

+136
-27
lines changed

apps/extension/src/Components/InternalEvents.tsx

+32-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react'
1+
import React, { useCallback, useEffect, useRef } from 'react'
22
import ReactDOM from 'react-dom'
33
import { useSputlitContext, VisualState } from '../Hooks/useSputlitContext'
44
import { getSelectionHTML } from '../Utils/getSelectionHTML'
@@ -28,6 +28,7 @@ export function InternalEvents() {
2828
}
2929

3030
const highlighter = new Highlighter()
31+
type Timeout = ReturnType<typeof setTimeout>
3132

3233
/**
3334
* `useToggleHandler` handles the keyboard events for toggling sputlit.
@@ -37,6 +38,26 @@ function useToggleHandler() {
3738
const { previewMode, setPreviewMode } = useEditorContext()
3839
const { saveIt } = useSaveChanges()
3940

41+
const timeoutRef = useRef<Timeout>()
42+
const runAnimateTimer = useCallback((vs: VisualState.animatingIn | VisualState.animatingOut) => {
43+
let ms = 0
44+
if (vs === VisualState.animatingIn) {
45+
ms = 200
46+
}
47+
if (vs === VisualState.animatingOut) {
48+
ms = 100
49+
}
50+
51+
clearTimeout(timeoutRef.current as Timeout)
52+
timeoutRef.current = setTimeout(() => {
53+
if (vs === VisualState.animatingIn) {
54+
setVisualState(VisualState.showing)
55+
} else if (vs === VisualState.animatingOut) {
56+
setVisualState(VisualState.hidden)
57+
}
58+
}, ms)
59+
}, [])
60+
4061
useEffect(() => {
4162
function messageHandler(request: any, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
4263
switch (request.type) {
@@ -53,9 +74,9 @@ function useToggleHandler() {
5374
setSelection(undefined)
5475
}
5576

56-
setVisualState(VisualState.showing)
77+
setVisualState(VisualState.animatingIn)
5778
} else {
58-
setVisualState(VisualState.hidden)
79+
setVisualState(VisualState.animatingOut)
5980
}
6081
sendResponse(true)
6182
}
@@ -64,7 +85,7 @@ function useToggleHandler() {
6485
function handleKeyDown(event: KeyboardEvent) {
6586
if (event.key === 'Escape') {
6687
if (previewMode) {
67-
setVisualState(VisualState.hidden)
88+
setVisualState(VisualState.animatingOut)
6889
setTooltipState({ visualState: VisualState.hidden })
6990
} else {
7091
setPreviewMode(true)
@@ -73,6 +94,13 @@ function useToggleHandler() {
7394
}
7495
}
7596

97+
switch (visualState) {
98+
case VisualState.animatingIn:
99+
case VisualState.animatingOut:
100+
runAnimateTimer(visualState)
101+
break
102+
}
103+
76104
// Listen for message from background script to see if sputlit is requested
77105
chrome.runtime.onMessage.addListener(messageHandler)
78106

apps/extension/src/Components/Sputlit/index.tsx

+90-5
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,109 @@
1-
import React from 'react'
1+
import React, { useEffect, useRef } from 'react'
22
import Search from '../Search'
33
import Content from '../Content'
44
import { useSputlitContext, VisualState } from '../../Hooks/useSputlitContext'
55
import { Main, Overlay, SputlitContainer, Wrapper } from './styled'
66

7+
const appearanceAnimationKeyframes = [
8+
{
9+
opacity: 0,
10+
transform: 'scale(0.99)'
11+
},
12+
{ opacity: 1, transform: 'scale(1.01)' },
13+
{ opacity: 1, transform: 'scale(1)' }
14+
]
15+
716
const Sputlit = () => {
8-
const setVisualState = useSputlitContext().setVisualState
17+
const { visualState, setVisualState } = useSputlitContext()
18+
19+
const outerRef = React.useRef<HTMLDivElement>(null)
20+
const innerRef = React.useRef<HTMLDivElement>(null)
21+
22+
const enterMs = 200
23+
const exitMs = 100
24+
25+
// Show/hide animation
26+
useEffect(() => {
27+
if (visualState === VisualState.showing) {
28+
return
29+
}
30+
31+
const duration = visualState === VisualState.animatingIn ? enterMs : exitMs
32+
33+
const element = outerRef.current
34+
35+
element?.animate(appearanceAnimationKeyframes, {
36+
duration,
37+
easing: visualState === VisualState.animatingOut ? 'ease-in' : 'ease-out',
38+
direction: visualState === VisualState.animatingOut ? 'reverse' : 'normal',
39+
fill: 'forwards'
40+
})
41+
}, [visualState])
42+
43+
// Height animation
44+
const previousHeight = useRef<number>()
45+
useEffect(() => {
46+
// Only animate if we're actually showing
47+
if (visualState === VisualState.showing) {
48+
const outer = outerRef.current
49+
const inner = innerRef.current
50+
51+
if (!outer || !inner) {
52+
return
53+
}
54+
55+
const ro = new ResizeObserver((entries) => {
56+
for (let entry of entries) {
57+
const cr = entry.contentRect
58+
59+
if (!previousHeight.current) {
60+
previousHeight.current = cr.height
61+
}
62+
63+
outer.animate(
64+
[
65+
{
66+
height: `${previousHeight.current}px`
67+
},
68+
{
69+
height: `${cr.height}px`
70+
}
71+
],
72+
{
73+
duration: enterMs / 2,
74+
easing: 'ease-out',
75+
fill: 'forwards'
76+
}
77+
)
78+
previousHeight.current = cr.height
79+
}
80+
})
81+
82+
ro.observe(inner)
83+
84+
return () => {
85+
ro.unobserve(inner)
86+
}
87+
}
88+
}, [visualState])
989

1090
return (
1191
<SputlitContainer id="sputlit-container">
12-
<Wrapper>
13-
<Main id="sputlit-main">
92+
<Wrapper
93+
ref={outerRef}
94+
// style={{
95+
// ...appearanceAnimationKeyframes[0]
96+
// }}
97+
>
98+
<Main id="sputlit-main" ref={innerRef}>
1499
<Search />
15100
<Content />
16101
</Main>
17102
</Wrapper>
18103
<Overlay
19104
id="sputlit-overlay"
20105
onClick={() => {
21-
setVisualState(VisualState.hidden)
106+
setVisualState(VisualState.animatingOut)
22107
}}
23108
/>
24109
</SputlitContainer>
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
import styled from 'styled-components'
1+
import styled, { keyframes } from 'styled-components'
22
import { normalize } from '@mexit/shared'
33

44
export const SputlitContainer = styled.div`
55
${normalize}
66
font-family: 'Inter', sans-serif;
7+
8+
position: fixed;
9+
display: flex;
10+
align-items: flex-start;
11+
justify-content: center;
12+
width: 100%;
13+
inset: 0;
14+
padding: 14vh 16px 16px;
715
`
816

917
export const Overlay = styled.div`
@@ -12,37 +20,25 @@ export const Overlay = styled.div`
1220
position: fixed;
1321
top: 0px;
1422
left: 0px;
15-
z-index: 9999;
23+
z-index: -1;
1624
opacity: 0.6;
17-
transition: all 0.1s cubic-bezier(0.05, 0.03, 0.35, 1);
1825
`
1926

2027
export const Wrapper = styled.div`
21-
position: fixed;
2228
width: 700px;
23-
border-radius: 8px;
2429
25-
margin: auto;
26-
top: 0px;
27-
right: 0px;
28-
bottom: 0px;
29-
left: 0px;
30-
z-index: 9999999999;
31-
height: 540px;
32-
transition: all 0.2s cubic-bezier(0.05, 0.03, 0.35, 1);
30+
overflow: hidden;
31+
background: ${({ theme }) => theme.colors.background.app};
32+
box-shadow: 0px 6px 20px rgb(0 0 0 / 20%);
33+
border-radius: 10px;
3334
`
3435

3536
export const Main = styled.div`
3637
position: absolute;
3738
width: 100%;
38-
background: ${({ theme }) => theme.colors.background.app};
39-
box-shadow: 0px 6px 20px rgb(0 0 0 / 20%);
4039
41-
border-radius: 10px;
4240
top: 0px;
4341
left: 0px;
4442
z-index: 9999999998;
45-
height: fit-content;
46-
transition: all 0.2s cubic-bezier(0.05, 0.03, 0.35, 1);
4743
display: block;
4844
`

0 commit comments

Comments
 (0)