@cntrl-site/sdk-nextjs 1.0.18 → 1.0.19-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/inspectionProfiles/Project_Default.xml +15 -0
- package/jest.config.js +2 -2
- package/lib/components/Article.js +9 -8
- package/lib/components/Item.js +33 -13
- package/lib/components/items/CodeEmbedItem.js +11 -8
- package/lib/components/items/CustomItem.js +10 -7
- package/lib/components/items/GroupItem.js +11 -8
- package/lib/components/items/ImageItem.js +21 -15
- package/lib/components/items/RectangleItem.js +11 -8
- package/lib/components/items/RichTextItem.js +10 -7
- package/lib/components/items/VideoItem.js +16 -12
- package/lib/components/items/VimeoEmbed.js +19 -15
- package/lib/components/items/YoutubeEmbed.js +18 -13
- package/lib/components/useStatesClassNames.js +18 -0
- package/lib/components/useStatesTransitions.js +89 -0
- package/lib/provider/InteractionsContext.js +45 -0
- package/lib/utils/{HoverStyles/HoverStyles.js → StateStyles/StateStyles.js} +36 -24
- package/lib/utils/getStatesCSS.js +16 -0
- package/package.json +7 -4
- package/src/components/Article.tsx +31 -28
- package/src/components/ArticleWrapper.tsx +1 -2
- package/src/components/Item.tsx +38 -18
- package/src/components/items/CodeEmbedItem.tsx +12 -9
- package/src/components/items/CustomItem.tsx +12 -9
- package/src/components/items/GroupItem.tsx +12 -9
- package/src/components/items/ImageItem.tsx +26 -19
- package/src/components/items/RectangleItem.tsx +13 -10
- package/src/components/items/RichTextItem.tsx +17 -9
- package/src/components/items/VideoItem.tsx +19 -15
- package/src/components/items/VimeoEmbed.tsx +20 -16
- package/src/components/items/YoutubeEmbed.tsx +20 -16
- package/src/components/useStatesClassNames.ts +23 -0
- package/src/components/useStatesTransitions.ts +95 -0
- package/src/provider/InteractionsContext.test.tsx +97 -0
- package/src/provider/InteractionsContext.tsx +65 -0
- package/src/utils/{HoverStyles/HoverStyles.ts → StateStyles/StateStyles.ts} +41 -27
- package/src/utils/getStatesCSS.ts +24 -0
- package/cntrl-site-sdk-nextjs-1.0.17.tgz +0 -0
- package/cntrl-site-sdk-nextjs-1.0.18.tgz +0 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { FC, useId, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import JSXStyle from 'styled-jsx/style';
|
|
3
3
|
import { CntrlColor } from '@cntrl-site/color';
|
|
4
|
-
import {
|
|
4
|
+
import { getLayoutStyles, VideoItem as TVideoItem } from '@cntrl-site/sdk';
|
|
5
5
|
import { ItemProps } from '../Item';
|
|
6
6
|
import { LinkWrapper } from '../LinkWrapper';
|
|
7
7
|
import { useFileItem } from './useFileItem';
|
|
8
8
|
import { useItemAngle } from '../useItemAngle';
|
|
9
9
|
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
10
|
-
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
11
10
|
import { useRegisterResize } from "../../common/useRegisterResize";
|
|
12
11
|
import { useLayoutContext } from '../useLayoutContext';
|
|
13
12
|
import { ScrollPlaybackVideo } from '../ScrollPlaybackVideo';
|
|
13
|
+
import { useStatesClassNames } from '../useStatesClassNames';
|
|
14
|
+
import { getStatesCSS } from '../../utils/getStatesCSS';
|
|
15
|
+
import { useStatesTransitions } from '../useStatesTransitions';
|
|
14
16
|
|
|
15
17
|
export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize }) => {
|
|
16
18
|
const id = useId();
|
|
@@ -22,8 +24,12 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
22
24
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
23
25
|
const layoutId = useLayoutContext();
|
|
24
26
|
const scrollPlayback = layoutId ? item.layoutParams[layoutId].scrollPlayback : null;
|
|
25
|
-
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state
|
|
27
|
+
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
|
|
28
|
+
const statesWrapperClassNames = useStatesClassNames(item.id, item.state, 'video-wrapper');
|
|
29
|
+
const statesVideoClassNames = useStatesClassNames(item.id, item.state, 'video');
|
|
26
30
|
const hasScrollPlayback = scrollPlayback !== null;
|
|
31
|
+
useStatesTransitions(ref, item.state, ['angle', 'opacity', 'blur']);
|
|
32
|
+
useStatesTransitions(videoRef.current, item.state, ['strokeWidth', 'radius', 'strokeColor']);
|
|
27
33
|
useRegisterResize(ref, onResize);
|
|
28
34
|
const inlineStyles = {
|
|
29
35
|
...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
|
|
@@ -34,7 +40,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
34
40
|
return (
|
|
35
41
|
<LinkWrapper url={item.link?.url} target={item.link?.target}>
|
|
36
42
|
<div
|
|
37
|
-
className={`video-wrapper-${item.id}`}
|
|
43
|
+
className={`video-wrapper-${item.id} ${statesWrapperClassNames}`}
|
|
38
44
|
ref={setRef}
|
|
39
45
|
style={{
|
|
40
46
|
...(opacity !== undefined ? { opacity } : {}),
|
|
@@ -48,7 +54,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
48
54
|
src={item.commonParams.url}
|
|
49
55
|
playbackParams={scrollPlayback}
|
|
50
56
|
style={inlineStyles}
|
|
51
|
-
className={`video video-playback-wrapper video-${item.id}`}
|
|
57
|
+
className={`video video-playback-wrapper video-${item.id} ${statesWrapperClassNames}`}
|
|
52
58
|
/>
|
|
53
59
|
) : (
|
|
54
60
|
<video
|
|
@@ -58,7 +64,7 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
58
64
|
muted
|
|
59
65
|
loop
|
|
60
66
|
playsInline
|
|
61
|
-
className={`video video-${item.id}`}
|
|
67
|
+
className={`video video-${item.id} ${statesVideoClassNames}`}
|
|
62
68
|
style={inlineStyles}
|
|
63
69
|
>
|
|
64
70
|
<source src={item.commonParams.url} />
|
|
@@ -92,26 +98,24 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
|
|
|
92
98
|
display: flex;
|
|
93
99
|
justify-content: center;
|
|
94
100
|
}
|
|
95
|
-
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams,
|
|
101
|
+
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
|
|
102
|
+
const wrapperStatesCSS = getStatesCSS(item.id, 'video-wrapper', ['angle', 'opacity', 'blur'], stateParams);
|
|
103
|
+
const videoStatesCSS = getStatesCSS(item.id, 'video', ['strokeWidth', 'radius', 'strokeColor'], stateParams);
|
|
96
104
|
return (`
|
|
97
105
|
.video-wrapper-${item.id} {
|
|
98
106
|
opacity: ${layoutParams.opacity};
|
|
99
107
|
transform: rotate(${area.angle}deg);
|
|
100
108
|
filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
|
|
101
|
-
transition:
|
|
102
|
-
}
|
|
103
|
-
.video-wrapper-${item.id}:hover {
|
|
104
|
-
${getHoverStyles<ArticleItemType.Video>(['angle', 'opacity', 'blur'], hoverParams)};
|
|
109
|
+
transition: all 0.2s ease;
|
|
105
110
|
}
|
|
106
111
|
.video-${item.id} {
|
|
107
112
|
border-color: ${CntrlColor.parse(layoutParams.strokeColor).fmt('rgba')};
|
|
108
113
|
border-radius: ${layoutParams.radius * 100}vw;
|
|
109
114
|
border-width: ${layoutParams.strokeWidth * 100}vw;
|
|
110
|
-
transition:
|
|
111
|
-
}
|
|
112
|
-
.video-wrapper-${item.id}:hover .video {
|
|
113
|
-
${getHoverStyles<ArticleItemType.Video>(['strokeWidth', 'radius', 'strokeColor'], hoverParams)};
|
|
115
|
+
transition: all 0.2s ease;
|
|
114
116
|
}
|
|
117
|
+
${wrapperStatesCSS}
|
|
118
|
+
${videoStatesCSS}
|
|
115
119
|
`);
|
|
116
120
|
})}
|
|
117
121
|
`}</JSXStyle>
|
|
@@ -5,10 +5,12 @@ import { ItemProps } from '../Item';
|
|
|
5
5
|
import { LinkWrapper } from '../LinkWrapper';
|
|
6
6
|
import { useEmbedVideoItem } from './useEmbedVideoItem';
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
|
-
import {
|
|
8
|
+
import { getLayoutStyles, VimeoEmbedItem as TVimeoEmbedItem } from '@cntrl-site/sdk';
|
|
9
9
|
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
10
|
-
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
11
10
|
import { useRegisterResize } from "../../common/useRegisterResize";
|
|
11
|
+
import { useStatesClassNames } from '../useStatesClassNames';
|
|
12
|
+
import { getStatesCSS } from '../../utils/getStatesCSS';
|
|
13
|
+
import { useStatesTransitions } from '../useStatesTransitions';
|
|
12
14
|
|
|
13
15
|
export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId, onResize }) => {
|
|
14
16
|
const id = useId();
|
|
@@ -21,8 +23,12 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
21
23
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
22
24
|
const [imgRef, setImgRef] = useState<HTMLImageElement | null>(null);
|
|
23
25
|
const [isCoverVisible, setIsCoverVisible] = useState(false);
|
|
24
|
-
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state
|
|
26
|
+
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
|
|
27
|
+
const wrapperClassNames = useStatesClassNames(item.id, item.state, 'embed-video-wrapper');
|
|
28
|
+
const videoClassNames = useStatesClassNames(item.id, item.state, 'embed-video');
|
|
25
29
|
useRegisterResize(ref, onResize);
|
|
30
|
+
useStatesTransitions(ref, item.state, ['angle', 'blur', 'opacity']);
|
|
31
|
+
useStatesTransitions(iframeRef, item.state, ['radius']);
|
|
26
32
|
const getValidVimeoUrl = (url: string): string => {
|
|
27
33
|
const validURL = new URL(url);
|
|
28
34
|
validURL.searchParams.append('controls', String(controls));
|
|
@@ -60,7 +66,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
60
66
|
return (
|
|
61
67
|
<LinkWrapper url={item.link?.url} target={item.link?.target}>
|
|
62
68
|
<div
|
|
63
|
-
className={`embed-video-wrapper-${item.id}`}
|
|
69
|
+
className={`embed-video-wrapper-${item.id} ${wrapperClassNames}`}
|
|
64
70
|
ref={setRef}
|
|
65
71
|
style={{
|
|
66
72
|
...(opacity !== undefined ? { opacity } : {}),
|
|
@@ -96,7 +102,7 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
96
102
|
)}
|
|
97
103
|
<iframe
|
|
98
104
|
ref={setIframeRef}
|
|
99
|
-
className=
|
|
105
|
+
className={`embed-video ${videoClassNames}`}
|
|
100
106
|
src={validUrl || ''}
|
|
101
107
|
allow="autoplay; fullscreen; picture-in-picture;"
|
|
102
108
|
allowFullScreen
|
|
@@ -111,31 +117,29 @@ export const VimeoEmbedItem: FC<ItemProps<TVimeoEmbedItem>> = ({ item, sectionId
|
|
|
111
117
|
width: 100%;
|
|
112
118
|
height: 100%;
|
|
113
119
|
}
|
|
114
|
-
.
|
|
120
|
+
.embed-video {
|
|
115
121
|
width: 100%;
|
|
116
122
|
height: 100%;
|
|
117
123
|
z-index: 1;
|
|
118
124
|
border: none;
|
|
119
125
|
overflow: hidden;
|
|
120
126
|
}
|
|
121
|
-
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams,
|
|
127
|
+
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
|
|
128
|
+
const wrapperStatesCSS = getStatesCSS(item.id, 'embed-video-wrapper', ['angle', 'blur', 'opacity'], stateParams);
|
|
129
|
+
const videoStatesCSS = getStatesCSS(item.id, 'embed-video', ['radius'], stateParams);
|
|
122
130
|
return (`
|
|
123
131
|
.embed-video-wrapper-${item.id} {
|
|
124
132
|
opacity: ${layoutParams.opacity};
|
|
125
133
|
transform: rotate(${area.angle}deg);
|
|
126
134
|
filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
|
|
127
|
-
transition:
|
|
135
|
+
transition: all 0.2s ease;
|
|
128
136
|
}
|
|
129
|
-
.embed-video-wrapper-${item.id}
|
|
130
|
-
${getHoverStyles<ArticleItemType.VimeoEmbed>(['angle', 'blur', 'opacity'], hoverParams)}
|
|
131
|
-
}
|
|
132
|
-
.embed-video-wrapper-${item.id} .embedVideo {
|
|
137
|
+
.embed-video-wrapper-${item.id} .embed-video {
|
|
133
138
|
border-radius: ${layoutParams.radius * 100}vw;
|
|
134
|
-
transition:
|
|
135
|
-
}
|
|
136
|
-
.embed-video-wrapper-${item.id}:hover .embedVideo {
|
|
137
|
-
${getHoverStyles<ArticleItemType.VimeoEmbed>(['radius'], hoverParams)};
|
|
139
|
+
transition: all 0.2s ease;
|
|
138
140
|
}
|
|
141
|
+
${wrapperStatesCSS}
|
|
142
|
+
${videoStatesCSS}
|
|
139
143
|
`);
|
|
140
144
|
})}
|
|
141
145
|
`}</JSXStyle>
|
|
@@ -5,12 +5,14 @@ import { LinkWrapper } from '../LinkWrapper';
|
|
|
5
5
|
import { getYoutubeId } from '../../utils/getValidYoutubeUrl';
|
|
6
6
|
import { useEmbedVideoItem } from './useEmbedVideoItem';
|
|
7
7
|
import { useItemAngle } from '../useItemAngle';
|
|
8
|
-
import {
|
|
9
|
-
import { getHoverStyles, getTransitions } from '../../utils/HoverStyles/HoverStyles';
|
|
8
|
+
import { getLayoutStyles, YoutubeEmbedItem as TYoutubeEmbedItem } from '@cntrl-site/sdk';
|
|
10
9
|
import { useCntrlContext } from '../../provider/useCntrlContext';
|
|
11
10
|
import { useYouTubeIframeApi } from '../../utils/Youtube/useYouTubeIframeApi';
|
|
12
11
|
import { YTPlayer } from '../../utils/Youtube/YoutubeIframeApi';
|
|
13
12
|
import { useRegisterResize } from "../../common/useRegisterResize";
|
|
13
|
+
import { useStatesClassNames } from '../useStatesClassNames';
|
|
14
|
+
import { getStatesCSS } from '../../utils/getStatesCSS';
|
|
15
|
+
import { useStatesTransitions } from '../useStatesTransitions';
|
|
14
16
|
|
|
15
17
|
export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, sectionId, onResize }) => {
|
|
16
18
|
const id = useId();
|
|
@@ -20,12 +22,16 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
20
22
|
const angle = useItemAngle(item, sectionId);
|
|
21
23
|
const YT = useYouTubeIframeApi();
|
|
22
24
|
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
|
25
|
+
const [wrapperRef, setWrapperRef] = useState<HTMLDivElement | null>(null);
|
|
23
26
|
const [player, setPlayer] = useState<YTPlayer | undefined>(undefined);
|
|
24
27
|
const [isCoverVisible, setIsCoverVisible] = useState(false);
|
|
25
28
|
const [imgRef, setImgRef] = useState<HTMLImageElement | null>(null);
|
|
26
|
-
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state
|
|
29
|
+
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams, item.state];
|
|
30
|
+
const wrapperClassNames = useStatesClassNames(item.id, item.state, 'embed-youtube-video-wrapper');
|
|
31
|
+
const embedClassNames = useStatesClassNames(item.id, item.state, 'embed');
|
|
27
32
|
useRegisterResize(div, onResize);
|
|
28
|
-
|
|
33
|
+
useStatesTransitions(wrapperRef, item.state, ['angle', 'blur', 'opacity']);
|
|
34
|
+
useStatesTransitions(div, item.state, ['radius']);
|
|
29
35
|
useEffect(() => {
|
|
30
36
|
const newUrl = new URL(url);
|
|
31
37
|
const videoId = getYoutubeId(newUrl);
|
|
@@ -78,7 +84,8 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
78
84
|
return (
|
|
79
85
|
<LinkWrapper url={item.link?.url} target={item.link?.target}>
|
|
80
86
|
<div
|
|
81
|
-
|
|
87
|
+
ref={setWrapperRef}
|
|
88
|
+
className={`embed-youtube-video-wrapper-${item.id} ${wrapperClassNames}`}
|
|
82
89
|
onMouseEnter={() => {
|
|
83
90
|
if (!player || play !== 'on-hover') return;
|
|
84
91
|
player.playVideo();
|
|
@@ -113,11 +120,10 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
113
120
|
/>
|
|
114
121
|
)}
|
|
115
122
|
<div
|
|
116
|
-
className={`embed-${item.id}`}
|
|
123
|
+
className={`embed-${item.id} ${embedClassNames}`}
|
|
117
124
|
ref={setDiv}
|
|
118
125
|
style={{
|
|
119
126
|
...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
|
|
120
|
-
|
|
121
127
|
}}
|
|
122
128
|
/>
|
|
123
129
|
</div>
|
|
@@ -137,24 +143,22 @@ export const YoutubeEmbedItem: FC<ItemProps<TYoutubeEmbedItem>> = ({ item, secti
|
|
|
137
143
|
z-index: 1;
|
|
138
144
|
border: none;
|
|
139
145
|
}
|
|
140
|
-
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams,
|
|
146
|
+
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams, stateParams]) => {
|
|
147
|
+
const wrapperStatesCSS = getStatesCSS(item.id, 'embed-youtube-video-wrapper', ['angle', 'blur', 'opacity'], stateParams);
|
|
148
|
+
const embedStatesCSS = getStatesCSS(item.id, 'embed', ['radius'], stateParams);
|
|
141
149
|
return (`
|
|
142
150
|
.embed-youtube-video-wrapper-${item.id} {
|
|
143
151
|
opacity: ${layoutParams.opacity};
|
|
144
152
|
transform: rotate(${area.angle}deg);
|
|
145
153
|
filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
|
|
146
|
-
transition:
|
|
154
|
+
transition: all 0.2s ease;
|
|
147
155
|
}
|
|
148
156
|
.embed-youtube-video-wrapper-${item.id} .embed-${item.id} {
|
|
149
157
|
border-radius: ${layoutParams.radius * 100}vw;
|
|
150
|
-
transition:
|
|
151
|
-
}
|
|
152
|
-
.embed-youtube-video-wrapper-${item.id}:hover {
|
|
153
|
-
${getHoverStyles<ArticleItemType.YoutubeEmbed>(['angle', 'blur', 'opacity'], hoverParams)};
|
|
154
|
-
}
|
|
155
|
-
.embed-youtube-video-wrapper-${item.id}:hover .embed-${item.id} {
|
|
156
|
-
${getHoverStyles<ArticleItemType.YoutubeEmbed>(['radius'], hoverParams)};
|
|
158
|
+
transition: all 0.2s ease;
|
|
157
159
|
}
|
|
160
|
+
${wrapperStatesCSS}
|
|
161
|
+
${embedStatesCSS}
|
|
158
162
|
`);
|
|
159
163
|
})}
|
|
160
164
|
`}</JSXStyle>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ItemStateParams } from '@cntrl-site/sdk';
|
|
2
|
+
import { useContext } from 'react';
|
|
3
|
+
import { InteractionsContext } from '../provider/InteractionsContext';
|
|
4
|
+
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
5
|
+
|
|
6
|
+
export function useStatesClassNames(
|
|
7
|
+
itemId: string,
|
|
8
|
+
itemStates: Record<LayoutId, Record<StateId, ItemStateParams>>,
|
|
9
|
+
uniquePrefix: string, // unique in terms of item type, ie. rectangle, code-embed, group etc
|
|
10
|
+
): string {
|
|
11
|
+
const { interactionsStatesMap } = useContext(InteractionsContext);
|
|
12
|
+
const { layoutId } = useCurrentLayout();
|
|
13
|
+
const activeStates = Object.values(interactionsStatesMap);
|
|
14
|
+
const statesForLayout = layoutId ? itemStates[layoutId] : {};
|
|
15
|
+
const stateClassNames = Object.keys(statesForLayout ?? {})
|
|
16
|
+
.filter((stateId) => activeStates.includes(stateId))
|
|
17
|
+
.map((stateId) => `${uniquePrefix}-${itemId}-state-${stateId}`)
|
|
18
|
+
.join(' ');
|
|
19
|
+
return stateClassNames;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type StateId = string;
|
|
23
|
+
type LayoutId = string;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ArticleItemType, ItemState, ItemStateParams, ItemStatesMap } from '@cntrl-site/sdk';
|
|
2
|
+
import { useContext, useEffect, useMemo } from 'react';
|
|
3
|
+
import { InteractionsContext } from '../provider/InteractionsContext';
|
|
4
|
+
import { useCurrentLayout } from '../common/useCurrentLayout';
|
|
5
|
+
|
|
6
|
+
type AllKeys<T> = T extends any ? keyof T : never;
|
|
7
|
+
type StatePropertyKey = AllKeys<ItemStatesMap[keyof ItemStatesMap]>;
|
|
8
|
+
|
|
9
|
+
export function useStatesTransitions(
|
|
10
|
+
el: HTMLElement | null,
|
|
11
|
+
state: ItemState<ArticleItemType>,
|
|
12
|
+
values: StatePropertyKey[]
|
|
13
|
+
) {
|
|
14
|
+
const { interactionsStatesMap } = useContext(InteractionsContext);
|
|
15
|
+
const { layoutId } = useCurrentLayout();
|
|
16
|
+
const activeStates = Object.values(interactionsStatesMap);
|
|
17
|
+
const statesForLayout = useMemo(() => layoutId ? state[layoutId] : {}, [layoutId]);
|
|
18
|
+
const itemStatesIds = Object.keys(statesForLayout);
|
|
19
|
+
const itemActiveStateId = activeStates.find((stateId) => itemStatesIds.includes(stateId));
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!itemActiveStateId || !el) return;
|
|
22
|
+
const state = statesForLayout[itemActiveStateId];
|
|
23
|
+
const transitionStr = getTransition(state, 'in', values);
|
|
24
|
+
const slowestProp = getSlowestProperty(state, 'in', values);
|
|
25
|
+
if (!transitionStr) return;
|
|
26
|
+
el.style.transition = transitionStr;
|
|
27
|
+
el.ontransitionend = (e) => {
|
|
28
|
+
e.stopPropagation();
|
|
29
|
+
if (e.target !== el || e.propertyName !== slowestProp) return;
|
|
30
|
+
el.style.transition = 'none';
|
|
31
|
+
};
|
|
32
|
+
return () => {
|
|
33
|
+
const transitionStr = getTransition(state, 'out', values);
|
|
34
|
+
const slowestProp = getSlowestProperty(state, 'out', values);
|
|
35
|
+
if (!transitionStr) return;
|
|
36
|
+
el.style.transition = transitionStr;
|
|
37
|
+
el.ontransitionend = (e) => {
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
if (e.target !== el || e.propertyName !== slowestProp) return;
|
|
40
|
+
el.style.transition = 'none';
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}, [itemActiveStateId, statesForLayout, el]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CSSPropertyNameMap: Record<keyof ItemState<ArticleItemType>, string> = {
|
|
47
|
+
'width': 'width',
|
|
48
|
+
'height': 'height',
|
|
49
|
+
'top': 'top',
|
|
50
|
+
'left': 'left',
|
|
51
|
+
'scale': 'transform',
|
|
52
|
+
'angle': 'transform',
|
|
53
|
+
'opacity': 'opacity',
|
|
54
|
+
'radius': 'border-radius',
|
|
55
|
+
'strokeWidth': 'border-width',
|
|
56
|
+
'strokeColor': 'border-color',
|
|
57
|
+
'fillColor': 'background-color',
|
|
58
|
+
'blur': 'filter',
|
|
59
|
+
'backdropBlur': 'backdrop-filter',
|
|
60
|
+
'letterSpacing': 'letter-spacing',
|
|
61
|
+
'wordSpacing': 'word-spacing',
|
|
62
|
+
'color': 'color'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function getTransition(
|
|
66
|
+
state: ItemStateParams,
|
|
67
|
+
direction: 'in' | 'out',
|
|
68
|
+
values: string[]
|
|
69
|
+
) {
|
|
70
|
+
return Object.entries(state)
|
|
71
|
+
.filter(([key]) => values.includes(key))
|
|
72
|
+
.map(([key, params]) => {
|
|
73
|
+
const cssKey = CSSPropertyNameMap[key];
|
|
74
|
+
if (!cssKey) {
|
|
75
|
+
throw new Error(`Cannot translate "${key}" to a CSS property.`);
|
|
76
|
+
}
|
|
77
|
+
return `${cssKey} ${params[direction].duration}ms ${params[direction].timing} ${params[direction].delay}ms`;
|
|
78
|
+
}, [])
|
|
79
|
+
.join(', ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getSlowestProperty(state: ItemStateParams, direction: 'in' | 'out', values: string[]): string {
|
|
83
|
+
const mappedEntries = Object.entries(state)
|
|
84
|
+
.filter(([key]) => values.includes(key))
|
|
85
|
+
.map(([key, params]) => {
|
|
86
|
+
const transitionParams = params[direction];
|
|
87
|
+
return {
|
|
88
|
+
key,
|
|
89
|
+
time: transitionParams.duration + transitionParams.delay
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (mappedEntries.length === 0) return '';
|
|
93
|
+
const { key } = mappedEntries.reduce((slowest, current) => current.time > slowest.time ? current : slowest);
|
|
94
|
+
return CSSPropertyNameMap[key];
|
|
95
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { InteractionsProvider, InteractionsContext } from './InteractionsContext';
|
|
4
|
+
import { Interaction } from '@cntrl-site/sdk';
|
|
5
|
+
|
|
6
|
+
describe('InteractionsProvider', () => {
|
|
7
|
+
const interactions: Interaction[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'interaction1',
|
|
10
|
+
startStateId: 'state1',
|
|
11
|
+
triggers: [
|
|
12
|
+
{ itemId: 'item1', type: 'click', from: 'state1', to: 'state2' }
|
|
13
|
+
],
|
|
14
|
+
states: [{ id: 'state1' }, { id: 'state2' }]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'interaction2',
|
|
18
|
+
startStateId: 'state3',
|
|
19
|
+
triggers: [
|
|
20
|
+
{ itemId: 'item2', type: 'hover-in', from: 'state3', to: 'state4' }
|
|
21
|
+
],
|
|
22
|
+
states: [{ id: 'state3' }, { id: 'state4' }]
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
it('should generate correct default interactionsStatesMap', () => {
|
|
27
|
+
let contextValue;
|
|
28
|
+
|
|
29
|
+
render(
|
|
30
|
+
<InteractionsProvider interactions={interactions}>
|
|
31
|
+
<InteractionsContext.Consumer>
|
|
32
|
+
{value => {
|
|
33
|
+
contextValue = value;
|
|
34
|
+
return null;
|
|
35
|
+
}}
|
|
36
|
+
</InteractionsContext.Consumer>
|
|
37
|
+
</InteractionsProvider>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(contextValue!).toBeDefined();
|
|
41
|
+
expect(contextValue!.interactionsStatesMap).toEqual({
|
|
42
|
+
interaction1: 'state1',
|
|
43
|
+
interaction2: 'state3'
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should correctly update interactionsStatesMap when transitionTo is called', async () => {
|
|
48
|
+
let contextValue;
|
|
49
|
+
|
|
50
|
+
render(
|
|
51
|
+
<InteractionsProvider interactions={interactions}>
|
|
52
|
+
<InteractionsContext.Consumer>
|
|
53
|
+
{value => {
|
|
54
|
+
contextValue = value;
|
|
55
|
+
return null;
|
|
56
|
+
}}
|
|
57
|
+
</InteractionsContext.Consumer>
|
|
58
|
+
</InteractionsProvider>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(contextValue!).toBeDefined();
|
|
62
|
+
contextValue!.transitionTo('interaction1', 'state2');
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(contextValue!.interactionsStatesMap['interaction1']).toBe('state2');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return the correct trigger using getItemTrigger', () => {
|
|
70
|
+
let contextValue;
|
|
71
|
+
|
|
72
|
+
render(
|
|
73
|
+
<InteractionsProvider interactions={interactions}>
|
|
74
|
+
<InteractionsContext.Consumer>
|
|
75
|
+
{value => {
|
|
76
|
+
contextValue = value;
|
|
77
|
+
return null;
|
|
78
|
+
}}
|
|
79
|
+
</InteractionsContext.Consumer>
|
|
80
|
+
</InteractionsProvider>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(contextValue!).toBeDefined();
|
|
84
|
+
|
|
85
|
+
// Check the correct trigger is returned
|
|
86
|
+
const trigger = contextValue!.getItemTrigger('item1', 'click');
|
|
87
|
+
expect(trigger).toEqual({
|
|
88
|
+
id: 'interaction1',
|
|
89
|
+
from: 'state1',
|
|
90
|
+
to: 'state2',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Check that no trigger is returned when conditions don't match
|
|
94
|
+
const noTrigger = contextValue!.getItemTrigger('item1', 'hover-on');
|
|
95
|
+
expect(noTrigger).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createContext, FC, PropsWithChildren, useState } from 'react';
|
|
2
|
+
import { Interaction, InteractionTrigger } from '@cntrl-site/sdk';
|
|
3
|
+
|
|
4
|
+
const defaultState = {
|
|
5
|
+
interactionsStatesMap: {},
|
|
6
|
+
interactions: [],
|
|
7
|
+
transitionTo: () => {},
|
|
8
|
+
getItemTrigger: () => null
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const InteractionsContext = createContext<{
|
|
12
|
+
interactionsStatesMap: StatesMap,
|
|
13
|
+
interactions: Interaction[],
|
|
14
|
+
transitionTo: (interactionId: string, stateId: string) => void,
|
|
15
|
+
getItemTrigger: (itemId: string, triggerType: TriggerType) => Trigger | null
|
|
16
|
+
}>(defaultState);
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
interactions: Interaction[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ interactions, children }) => {
|
|
23
|
+
const defaultStatesMap = interactions.reduce<Record<string, string>>((map, { id, startStateId }) => {
|
|
24
|
+
map[id] = startStateId;
|
|
25
|
+
return map;
|
|
26
|
+
}, {});
|
|
27
|
+
const [interactionsStatesMap, setInteractionsStatesMap] = useState(defaultStatesMap);
|
|
28
|
+
const transitionTo = (interactionId: string, stateId: string) => {
|
|
29
|
+
setInteractionsStatesMap((map) => ({ ...map, [interactionId]: stateId }));
|
|
30
|
+
};
|
|
31
|
+
const getItemTrigger = (itemId: string, triggerType: TriggerType): Trigger | null => {
|
|
32
|
+
for (const interaction of interactions) {
|
|
33
|
+
const activeStateId = interactionsStatesMap[interaction.id];
|
|
34
|
+
const matchingTrigger = interaction.triggers.find((trigger) =>
|
|
35
|
+
trigger.itemId === itemId &&
|
|
36
|
+
trigger.from === activeStateId &&
|
|
37
|
+
trigger.type === triggerType
|
|
38
|
+
);
|
|
39
|
+
if (matchingTrigger) {
|
|
40
|
+
return {
|
|
41
|
+
id: interaction.id,
|
|
42
|
+
from: matchingTrigger.from,
|
|
43
|
+
to: matchingTrigger.to,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
return (
|
|
50
|
+
<InteractionsContext.Provider value={{
|
|
51
|
+
transitionTo,
|
|
52
|
+
interactionsStatesMap,
|
|
53
|
+
interactions,
|
|
54
|
+
getItemTrigger
|
|
55
|
+
}}>
|
|
56
|
+
{children}
|
|
57
|
+
</InteractionsContext.Provider>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type StatesMap = Record<InteractionId, StateId>;
|
|
62
|
+
type Trigger = { id: InteractionId, from: StateId, to: StateId };
|
|
63
|
+
type TriggerType = InteractionTrigger['type'];
|
|
64
|
+
type InteractionId = string;
|
|
65
|
+
type StateId = string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StateParams, ArticleItemType, AnchorSide, ItemState, ItemStatesMap } from '@cntrl-site/sdk';
|
|
2
2
|
import { CntrlColor } from '@cntrl-site/color';
|
|
3
3
|
import { getItemTopStyle } from '../getItemTopStyle';
|
|
4
4
|
|
|
5
|
-
type
|
|
6
|
-
type
|
|
7
|
-
type
|
|
5
|
+
type StateParamsGetter = (value: any, anchorSide?: AnchorSide) => string;
|
|
6
|
+
type AllKeys<T> = T extends any ? keyof T : never;
|
|
7
|
+
type StatePropertyKey = AllKeys<ItemStatesMap[keyof ItemStatesMap]>;
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const stateTransformationMap: Record<StatePropertyKey, StateParamsGetter> = {
|
|
10
10
|
'width': (width: number) => `width: ${width * 100}vw !important;`,
|
|
11
11
|
'height': (height: number) => `height: ${height * 100}vw !important;`,
|
|
12
12
|
'top': (top: number, anchorSide?: AnchorSide) => `top: ${getItemTopStyle(top, anchorSide)} !important;`,
|
|
@@ -25,7 +25,7 @@ const hoverTransformationMap: Record<keyof ItemHoverParams, HoverParamsGetter> =
|
|
|
25
25
|
'wordSpacing': (wordSpacing: number) => `word-spacing: ${wordSpacing * 100}vw !important;`
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const CSSPropertyNameMap: Record<keyof
|
|
28
|
+
const CSSPropertyNameMap: Record<keyof ItemState<ArticleItemType>, string> = {
|
|
29
29
|
'width': 'width',
|
|
30
30
|
'height': 'height',
|
|
31
31
|
'top': 'top',
|
|
@@ -44,42 +44,56 @@ const CSSPropertyNameMap: Record<keyof ItemHoverParams, string> = {
|
|
|
44
44
|
'color': 'color'
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
export function
|
|
48
|
-
values: Array<
|
|
49
|
-
|
|
47
|
+
export function getStateStyles<T extends ArticleItemType>(
|
|
48
|
+
values: Array<StatePropertyKey>,
|
|
49
|
+
state?: ItemStatesMap[T],
|
|
50
|
+
anchorSide?: AnchorSide
|
|
50
51
|
): string {
|
|
51
|
-
if (!
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (!state) return '';
|
|
53
|
+
|
|
54
|
+
const stateValues = values.reduce<string[]>((acc, valueName) => {
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
if (valueName in state && state[valueName] !== undefined) {
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
const stateProperties = state[valueName] as StateParams<string | number>;
|
|
59
|
+
const getter = stateTransformationMap[valueName] as StateParamsGetter;
|
|
55
60
|
return [
|
|
56
61
|
...acc,
|
|
57
|
-
|
|
62
|
+
getter(stateProperties.value, anchorSide)
|
|
58
63
|
];
|
|
59
64
|
}
|
|
60
65
|
return acc;
|
|
61
66
|
}, []);
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
|
|
68
|
+
if (!stateValues.length) return '';
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
const transitionStr = getTransitions(values, state);
|
|
71
|
+
stateValues.push(`transition: ${transitionStr};`);
|
|
72
|
+
return stateValues.join('\n');
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
export function
|
|
67
|
-
values: Array<
|
|
68
|
-
|
|
69
|
-
anchorSide?: AnchorSide
|
|
75
|
+
export function getTransitions<T extends ArticleItemType>(
|
|
76
|
+
values: Array<StatePropertyKey>,
|
|
77
|
+
state?: ItemStatesMap[T]
|
|
70
78
|
): string {
|
|
71
|
-
if (!
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if (!state) return 'unset';
|
|
80
|
+
const transitionValues = values.reduce<string[]>((acc, valueName) => {
|
|
81
|
+
// @ts-ignore TODO
|
|
82
|
+
if (valueName in state && state[valueName] !== undefined) {
|
|
83
|
+
// @ts-ignore TODO
|
|
84
|
+
const stateProperties = state[valueName] as StateParams<string | number>;
|
|
75
85
|
return [
|
|
76
86
|
...acc,
|
|
77
|
-
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
`${CSSPropertyNameMap[valueName]} ${stateProperties!.duration}ms ${stateProperties!.timing} ${stateProperties!.delay}ms`
|
|
78
89
|
];
|
|
79
90
|
}
|
|
80
91
|
return acc;
|
|
81
92
|
}, []);
|
|
82
|
-
if (!
|
|
83
|
-
return
|
|
93
|
+
if (!transitionValues.length) return 'unset';
|
|
94
|
+
return transitionValues.join(', ');
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
// export function getStateTransitions<T extends ArticleItemType>(
|
|
98
|
+
//
|
|
99
|
+
// )
|