@cntrl-site/sdk-nextjs 1.8.40-alpha.0 → 1.8.40
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/lib/components/items/FileItem/ImageItem.js +22 -46
- package/lib/components/items/FileItem/VideoItem.js +21 -45
- package/lib/components/items/FileItem/useFileItem.js +5 -12
- package/lib/components/items/RectangleItem/RectangleItem.js +22 -78
- package/lib/components/items/RectangleItem/useRectangleItem.js +8 -15
- package/lib/interactions/CSSPropertyNameMap.js +18 -18
- package/lib/utils/Animator/Animator.js +19 -171
- package/package.json +2 -2
- package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +1 -1
- package/src/components/items/ComponentItem/ComponentItem.tsx +1 -1
- package/src/components/items/CompoundItem/CompoundChild.tsx +2 -2
- package/src/components/items/CompoundItem/CompoundItem.tsx +1 -1
- package/src/components/items/CustomItem/CustomItem.tsx +1 -1
- package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +2 -2
- package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +2 -2
- package/src/components/items/FileItem/ImageItem.tsx +11 -60
- package/src/components/items/FileItem/VideoItem.tsx +12 -61
- package/src/components/items/FileItem/useFileItem.ts +6 -14
- package/src/components/items/GroupItem/GroupItem.tsx +1 -1
- package/src/components/items/Item.tsx +2 -2
- package/src/components/items/RectangleItem/RectangleItem.tsx +24 -126
- package/src/components/items/RectangleItem/useRectangleItem.ts +10 -17
- package/src/components/items/RichTextItem/RichTextItem.tsx +5 -5
- package/src/interactions/CSSPropertyNameMap.ts +19 -18
- package/src/interactions/ItemInteractionCtrl.ts +4 -4
- package/src/interactions/types.ts +7 -7
- package/src/provider/InteractionsContext.old.tsx +66 -0
- package/src/provider/InteractionsContext.test.tsx +97 -0
- package/src/utils/Animator/Animator.ts +27 -227
- package/src/utils/getStyleFromItemStateAndParams.ts +2 -2
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/modules.xml +0 -8
- package/.idea/sdk-nextjs.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/lib/utils/areFillsVisible/areFillsVisible.js +0 -23
- package/lib/utils/getFill.js +0 -43
- package/src/utils/areFillsVisible/areFillsVisible.test.ts +0 -63
- package/src/utils/areFillsVisible/areFillsVisible.ts +0 -37
- package/src/utils/getFill.ts +0 -46
|
@@ -9,30 +9,29 @@ import { useItemAngle } from '../useItemAngle';
|
|
|
9
9
|
import { useCntrlContext } from '../../../provider/useCntrlContext';
|
|
10
10
|
import { useRegisterResize } from '../../../common/useRegisterResize';
|
|
11
11
|
import { getStyleFromItemStateAndParams } from '../../../utils/getStyleFromItemStateAndParams';
|
|
12
|
-
import { FillLayer } from '@cntrl-site/sdk/dist/types/article/Item';
|
|
13
|
-
import { getFill } from '../../../utils/getFill';
|
|
14
|
-
import { areFillsVisible } from '../../../utils/areFillsVisible/areFillsVisible';
|
|
15
12
|
|
|
16
13
|
export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId, onResize, interactionCtrl, onVisibilityChange }) => {
|
|
17
14
|
const id = useId();
|
|
18
15
|
const { layouts } = useCntrlContext();
|
|
19
16
|
const {
|
|
20
|
-
|
|
17
|
+
fillColor: itemFillColor,
|
|
21
18
|
radius: itemRadius,
|
|
22
19
|
strokeWidth: itemStrokeWidth,
|
|
23
|
-
|
|
20
|
+
strokeColor: itemStrokeColor,
|
|
24
21
|
blur: itemBlur,
|
|
25
22
|
backdropBlur: itemBackdropBlur
|
|
26
23
|
} = useRectangleItem(item, sectionId);
|
|
27
24
|
const itemAngle = useItemAngle(item, sectionId);
|
|
28
|
-
const stateParams = interactionCtrl?.getState
|
|
29
|
-
const stateFillParams = interactionCtrl?.getState<FillLayer[]>(['fill']);
|
|
30
|
-
const stateFillLayers = stateFillParams?.styles?.fill;
|
|
31
|
-
const solidTransition = stateFillParams?.transition ?? 'none';
|
|
32
|
-
const stateStrokeFillParams = interactionCtrl?.getState<FillLayer[]>(['strokeFill']);
|
|
33
|
-
const stateStrokeFillLayers = stateStrokeFillParams?.styles?.strokeFill;
|
|
34
|
-
const strokeSolidTransition = stateStrokeFillParams?.transition ?? 'none';
|
|
25
|
+
const stateParams = interactionCtrl?.getState(['angle', 'fillColor', 'strokeWidth', 'radius', 'strokeColor', 'blur', 'backdropBlur']);
|
|
35
26
|
const styles = stateParams?.styles ?? {};
|
|
27
|
+
const backgroundColor = useMemo(() => {
|
|
28
|
+
const fillColor = getStyleFromItemStateAndParams(styles?.fillColor, itemFillColor);
|
|
29
|
+
return fillColor ? CntrlColor.parse(fillColor) : undefined;
|
|
30
|
+
}, [itemFillColor, styles?.fillColor]);
|
|
31
|
+
const borderColor = useMemo(() => {
|
|
32
|
+
const strokeColor = getStyleFromItemStateAndParams(styles?.strokeColor, itemStrokeColor);
|
|
33
|
+
return strokeColor ? CntrlColor.parse(strokeColor) : undefined;
|
|
34
|
+
}, [itemStrokeColor, styles?.strokeColor]);
|
|
36
35
|
const layoutValues: Record<string, any>[] = [item.area, item.layoutParams];
|
|
37
36
|
const [ref, setRef] = useState<HTMLDivElement | null>(null);
|
|
38
37
|
useRegisterResize(ref, onResize);
|
|
@@ -42,18 +41,11 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId,
|
|
|
42
41
|
const angle = getStyleFromItemStateAndParams(styles?.angle, itemAngle);
|
|
43
42
|
const blur = getStyleFromItemStateAndParams(styles?.blur, itemBlur);
|
|
44
43
|
const backdropFilterValue = backdropBlur ? `blur(${backdropBlur * 100}vw)` : undefined;
|
|
45
|
-
const isInteractive =
|
|
44
|
+
const isInteractive = backgroundColor?.getAlpha() !== 0 || (strokeWidth !== 0 && borderColor?.getAlpha() !== 0);
|
|
46
45
|
useEffect(() => {
|
|
47
46
|
onVisibilityChange?.(isInteractive);
|
|
48
47
|
}, [isInteractive, onVisibilityChange]);
|
|
49
48
|
|
|
50
|
-
const strokeValue = stateStrokeFillLayers
|
|
51
|
-
? getStyleFromItemStateAndParams<FillLayer>(stateStrokeFillLayers[0], itemStrokeFill?.[0])
|
|
52
|
-
: itemStrokeFill?.[0];
|
|
53
|
-
const stroke = strokeValue
|
|
54
|
-
? getFill(strokeValue) ?? 'transparent'
|
|
55
|
-
: 'transparent';
|
|
56
|
-
|
|
57
49
|
return (
|
|
58
50
|
<LinkWrapper url={item.link?.url} target={item.link?.target}>
|
|
59
51
|
<>
|
|
@@ -61,15 +53,10 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId,
|
|
|
61
53
|
className={`rectangle-${item.id}`}
|
|
62
54
|
ref={setRef}
|
|
63
55
|
style={{
|
|
64
|
-
...(
|
|
65
|
-
|
|
66
|
-
...(strokeValue.type === 'image' ? {
|
|
67
|
-
backgroundPosition: 'center',
|
|
68
|
-
backgroundSize: strokeValue.behavior === 'repeat' ? `${strokeValue.backgroundSize}%` : strokeValue.behavior,
|
|
69
|
-
backgroundRepeat: strokeValue.behavior === 'repeat' ? 'repeat' : 'no-repeat'
|
|
70
|
-
} : {})
|
|
71
|
-
} : {}),
|
|
56
|
+
...(backgroundColor ? { backgroundColor: `${backgroundColor.fmt('rgba')}` } : {}),
|
|
57
|
+
...(borderColor ? { borderColor: `${borderColor.fmt('rgba')}` } : {}),
|
|
72
58
|
...(radius !== undefined ? { borderRadius: `${radius * 100}vw` } : {}),
|
|
59
|
+
...(strokeWidth !== undefined ? { borderWidth: `${strokeWidth * 100}vw` } : {}),
|
|
73
60
|
...(angle !== undefined ? { transform: `rotate(${angle}deg)` } : {}),
|
|
74
61
|
...(blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {}),
|
|
75
62
|
willChange: blur !== 0 && blur !== undefined ? 'transform' : 'unset',
|
|
@@ -79,121 +66,32 @@ export const RectangleItem: FC<ItemProps<TRectangleItem>> = ({ item, sectionId,
|
|
|
79
66
|
),
|
|
80
67
|
transition: stateParams?.transition ?? 'none'
|
|
81
68
|
}}
|
|
82
|
-
|
|
83
|
-
<div
|
|
84
|
-
className={`rectangle-border-${item.id}`}
|
|
85
|
-
style={{
|
|
86
|
-
position: 'absolute',
|
|
87
|
-
inset: 0,
|
|
88
|
-
borderRadius: 'inherit',
|
|
89
|
-
pointerEvents: 'none',
|
|
90
|
-
zIndex: 2,
|
|
91
|
-
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
|
92
|
-
WebkitMaskComposite: 'xor',
|
|
93
|
-
maskComposite: 'exclude',
|
|
94
|
-
...(strokeWidth !== 0 && strokeValue ? {
|
|
95
|
-
...(strokeWidth ? { padding: `${strokeWidth * 100}vw` } : {}),
|
|
96
|
-
...(strokeValue.type === 'solid' ? { transition: strokeSolidTransition, background: stroke } : {}),
|
|
97
|
-
...(strokeValue.type === 'image' ? {
|
|
98
|
-
backgroundPosition: 'center',
|
|
99
|
-
backgroundSize: strokeValue.behavior === 'repeat' ? `${strokeValue.backgroundSize}%` : strokeValue.behavior,
|
|
100
|
-
backgroundRepeat: strokeValue.behavior === 'repeat' ? 'repeat' : 'no-repeat'
|
|
101
|
-
} : {
|
|
102
|
-
background: stroke,
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
} : { background: stroke }),
|
|
106
|
-
}}
|
|
107
|
-
/>
|
|
108
|
-
{itemFill && itemFill.map((fill) => {
|
|
109
|
-
const stateFillLayer = stateFillLayers?.find((layer) => layer.id === fill.id);
|
|
110
|
-
const value = stateFillLayer
|
|
111
|
-
? (getStyleFromItemStateAndParams<FillLayer>(stateFillLayer, fill) ?? fill)
|
|
112
|
-
: fill;
|
|
113
|
-
const background = value
|
|
114
|
-
? getFill(value) ?? 'transparent'
|
|
115
|
-
: 'transparent';
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<Fill fill={value} itemId={item.id} background={background} solidTransition={solidTransition} />
|
|
119
|
-
);
|
|
120
|
-
})}
|
|
121
|
-
</div>
|
|
69
|
+
/>
|
|
122
70
|
<JSXStyle id={id}>{`
|
|
123
71
|
.rectangle-${item.id} {
|
|
124
72
|
position: absolute;
|
|
125
73
|
width: 100%;
|
|
126
74
|
height: 100%;
|
|
75
|
+
border-style: solid;
|
|
127
76
|
box-sizing: border-box;
|
|
128
|
-
}
|
|
129
|
-
.image-fill-${item.id} {
|
|
130
|
-
position: absolute;
|
|
131
|
-
top: 0;
|
|
132
|
-
left: 0;
|
|
133
|
-
width: 100%;
|
|
134
|
-
height: 100%;
|
|
135
|
-
transform-origin: center center;
|
|
136
|
-
z-index: 1;
|
|
137
|
-
background-position: center;
|
|
138
|
-
},
|
|
77
|
+
}
|
|
139
78
|
${getLayoutStyles(layouts, layoutValues, ([area, layoutParams]) => {
|
|
140
|
-
|
|
79
|
+
return (`
|
|
141
80
|
.rectangle-${item.id} {
|
|
142
|
-
|
|
81
|
+
background-color: ${CntrlColor.parse(layoutParams.fillColor).fmt('rgba')};
|
|
82
|
+
border-color: ${CntrlColor.parse(layoutParams.strokeColor).fmt('rgba')};
|
|
83
|
+
border-radius: ${layoutParams.radius * 100}vw;
|
|
84
|
+
border-width: ${layoutParams.strokeWidth * 100}vw;
|
|
143
85
|
transform: rotate(${area.angle}deg);
|
|
144
86
|
filter: ${layoutParams.blur !== 0 ? `blur(${layoutParams.blur * 100}vw)` : 'unset'};
|
|
145
87
|
${layoutParams.blur !== 0 ? 'will-change: transform;' : ''}
|
|
146
88
|
backdrop-filter: ${layoutParams.backdropBlur !== 0 ? `blur(${layoutParams.backdropBlur * 100}vw)` : 'unset'};
|
|
147
89
|
-webkit-backdrop-filter: ${layoutParams.backdropBlur !== 0 ? `blur(${layoutParams.backdropBlur * 100}vw)` : 'unset'};
|
|
148
|
-
},
|
|
149
|
-
.rectangle-border-${item.id} {
|
|
150
|
-
position: absolute;
|
|
151
|
-
inset: 0;
|
|
152
|
-
border-radius: inherit;
|
|
153
|
-
pointer-events: none;
|
|
154
|
-
z-index: 2;
|
|
155
|
-
-webkit-mask:
|
|
156
|
-
linear-gradient(#fff 0 0) content-box,
|
|
157
|
-
linear-gradient(#fff 0 0);
|
|
158
|
-
-webkit-mask-composite: xor;
|
|
159
|
-
mask-composite: exclude;
|
|
160
90
|
}
|
|
161
91
|
`);
|
|
162
|
-
|
|
92
|
+
})}
|
|
163
93
|
`}</JSXStyle>
|
|
164
94
|
</>
|
|
165
95
|
</LinkWrapper>
|
|
166
96
|
);
|
|
167
97
|
};
|
|
168
|
-
|
|
169
|
-
function Fill({ fill, itemId, background, solidTransition }: { fill: FillLayer; itemId: string; background: string; solidTransition: string; }) {
|
|
170
|
-
const isRotatedImage = fill.type === 'image' && fill.rotation && fill.rotation !== 0;
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<div
|
|
174
|
-
key={fill.id}
|
|
175
|
-
className={fill.type === 'image' ? `image-fill-${itemId}` : `fill-${itemId}`}
|
|
176
|
-
style={{
|
|
177
|
-
...(fill.type === 'solid' ? { background, transition: solidTransition } : {}),
|
|
178
|
-
...(fill.type === 'image'
|
|
179
|
-
? {
|
|
180
|
-
transform: `rotate(${fill.rotation}deg)`,
|
|
181
|
-
backgroundImage: `url(${fill.src})`,
|
|
182
|
-
backgroundSize: fill.behavior === 'repeat' ? `${fill.backgroundSize}%` : fill.behavior,
|
|
183
|
-
backgroundRepeat: fill.behavior === 'repeat' ? 'repeat' : 'no-repeat',
|
|
184
|
-
opacity: fill.opacity
|
|
185
|
-
}
|
|
186
|
-
: { background }),
|
|
187
|
-
position: 'absolute',
|
|
188
|
-
mixBlendMode: fill.blendMode as any,
|
|
189
|
-
top: 0,
|
|
190
|
-
left: 0,
|
|
191
|
-
width: '100%',
|
|
192
|
-
height: '100%',
|
|
193
|
-
pointerEvents: 'none',
|
|
194
|
-
...(isRotatedImage ? { overflow: 'hidden' } : {})
|
|
195
|
-
}}
|
|
196
|
-
>
|
|
197
|
-
</div>
|
|
198
|
-
);
|
|
199
|
-
};
|
|
@@ -2,14 +2,7 @@ import { KeyframeType, RectangleItem } from '@cntrl-site/sdk';
|
|
|
2
2
|
import { useKeyframeValue } from '../../../common/useKeyframeValue';
|
|
3
3
|
import { useLayoutContext } from '../../useLayoutContext';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
{
|
|
7
|
-
id: 'default',
|
|
8
|
-
type: 'solid' as const,
|
|
9
|
-
value: 'rgba(0, 0, 0, 1)',
|
|
10
|
-
blendMode: 'normal'
|
|
11
|
-
}
|
|
12
|
-
];
|
|
5
|
+
const defaultColor = 'rgba(0, 0, 0, 1)';
|
|
13
6
|
|
|
14
7
|
export function useRectangleItem(item: RectangleItem, sectionId: string) {
|
|
15
8
|
const layoutId = useLayoutContext();
|
|
@@ -37,27 +30,27 @@ export function useRectangleItem(item: RectangleItem, sectionId: string) {
|
|
|
37
30
|
sectionId,
|
|
38
31
|
[layoutId]
|
|
39
32
|
);
|
|
40
|
-
const
|
|
33
|
+
const fillColor = useKeyframeValue(
|
|
41
34
|
item,
|
|
42
|
-
KeyframeType.
|
|
35
|
+
KeyframeType.Color,
|
|
43
36
|
(item, layoutId) => {
|
|
44
37
|
if (!layoutId) return;
|
|
45
38
|
const layoutParams = item.layoutParams[layoutId];
|
|
46
|
-
return '
|
|
39
|
+
return 'fillColor' in layoutParams ? layoutParams.fillColor : defaultColor;
|
|
47
40
|
},
|
|
48
|
-
(animator, scroll, value) => value ? animator.
|
|
41
|
+
(animator, scroll, value) => value ? animator.getColor({ color: value }, scroll).color : undefined,
|
|
49
42
|
sectionId,
|
|
50
43
|
[layoutId]
|
|
51
44
|
);
|
|
52
|
-
const
|
|
45
|
+
const strokeColor = useKeyframeValue(
|
|
53
46
|
item,
|
|
54
|
-
KeyframeType.
|
|
47
|
+
KeyframeType.BorderColor,
|
|
55
48
|
(item, layoutId) => {
|
|
56
49
|
if (!layoutId) return;
|
|
57
50
|
const layoutParams = item.layoutParams[layoutId];
|
|
58
|
-
return '
|
|
51
|
+
return 'strokeColor' in layoutParams ? layoutParams.strokeColor : defaultColor;
|
|
59
52
|
},
|
|
60
|
-
(animator, scroll, value) => value ? animator.
|
|
53
|
+
(animator, scroll, value) => value ? animator.getBorderColor({ color: value }, scroll).color : undefined,
|
|
61
54
|
sectionId,
|
|
62
55
|
[layoutId]
|
|
63
56
|
);
|
|
@@ -85,5 +78,5 @@ export function useRectangleItem(item: RectangleItem, sectionId: string) {
|
|
|
85
78
|
sectionId,
|
|
86
79
|
[layoutId]
|
|
87
80
|
);
|
|
88
|
-
return {
|
|
81
|
+
return { fillColor, strokeWidth, radius, strokeColor, blur, backdropBlur };
|
|
89
82
|
}
|
|
@@ -30,11 +30,11 @@ export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, on
|
|
|
30
30
|
const exemplary = useExemplary();
|
|
31
31
|
const { layoutId } = useCurrentLayout();
|
|
32
32
|
useRegisterResize(ref, onResize);
|
|
33
|
-
const stateParams = interactionCtrl?.getState
|
|
33
|
+
const stateParams = interactionCtrl?.getState(['angle', 'blur', 'letterSpacing', 'wordSpacing', 'color']);
|
|
34
34
|
const stateStyles = stateParams?.styles ?? {};
|
|
35
35
|
const transition = stateParams?.transition ?? 'none';
|
|
36
36
|
const textColor = useMemo(() => {
|
|
37
|
-
const color = getStyleFromItemStateAndParams(stateParams?.styles?.color
|
|
37
|
+
const color = getStyleFromItemStateAndParams(stateParams?.styles?.color, itemColor);
|
|
38
38
|
return color ? CntrlColor.parse(color) : undefined;
|
|
39
39
|
}, [itemColor, stateStyles.color]);
|
|
40
40
|
const angle = getStyleFromItemStateAndParams(stateStyles.angle, itemAngle);
|
|
@@ -60,11 +60,11 @@ export const RichTextItem: FC<ItemProps<TRichTextItem>> = ({ item, sectionId, on
|
|
|
60
60
|
ref={setRef}
|
|
61
61
|
className={`rich-text-wrapper-${item.id}`}
|
|
62
62
|
style={{
|
|
63
|
-
...(blur !== undefined ? { filter: `blur(${blur
|
|
63
|
+
...(blur !== undefined ? { filter: `blur(${blur * 100}vw)` } : {}),
|
|
64
64
|
...(textColor ? { color: `${textColor.fmt('rgba')}` } : {}),
|
|
65
65
|
...(angle !== undefined ? { transform: `rotate(${angle}deg)` } : {}),
|
|
66
|
-
...(letterSpacing !== undefined ? { letterSpacing: `${letterSpacing
|
|
67
|
-
...(wordSpacing !== undefined ? { wordSpacing: `${wordSpacing
|
|
66
|
+
...(letterSpacing !== undefined ? { letterSpacing: `${letterSpacing * exemplary}px` } : {}),
|
|
67
|
+
...(wordSpacing !== undefined ? { wordSpacing: `${wordSpacing * exemplary}px` } : {}),
|
|
68
68
|
...(fontSize !== undefined ? { fontSize: `${Math.round(fontSize * exemplary)}px` } : {}),
|
|
69
69
|
...(lineHeight !== undefined ? { lineHeight: `${lineHeight * exemplary}px` } : {}),
|
|
70
70
|
willChange: blur !== 0 && blur !== undefined ? 'transform' : 'unset',
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
import { ArticleItemType, ItemState } from '@cntrl-site/sdk';
|
|
2
2
|
|
|
3
3
|
export const CSSPropertyNameMap: Record<keyof ItemState<ArticleItemType>, string> = {
|
|
4
|
-
width: 'width',
|
|
5
|
-
height: 'height',
|
|
6
|
-
top: 'top',
|
|
7
|
-
left: 'left',
|
|
8
|
-
scale: 'transform',
|
|
9
|
-
angle: 'transform',
|
|
10
|
-
opacity: 'opacity',
|
|
11
|
-
radius: 'border-radius',
|
|
12
|
-
strokeWidth: 'border-width',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
blur: 'filter',
|
|
16
|
-
backdropBlur: 'backdrop-filter',
|
|
17
|
-
letterSpacing: 'letter-spacing',
|
|
18
|
-
wordSpacing: 'word-spacing',
|
|
19
|
-
color: 'color'
|
|
4
|
+
'width': 'width',
|
|
5
|
+
'height': 'height',
|
|
6
|
+
'top': 'top',
|
|
7
|
+
'left': 'left',
|
|
8
|
+
'scale': 'transform',
|
|
9
|
+
'angle': 'transform',
|
|
10
|
+
'opacity': 'opacity',
|
|
11
|
+
'radius': 'border-radius',
|
|
12
|
+
'strokeWidth': 'border-width',
|
|
13
|
+
'strokeColor': 'border-color',
|
|
14
|
+
'fillColor': 'background-color',
|
|
15
|
+
'blur': 'filter',
|
|
16
|
+
'backdropBlur': 'backdrop-filter',
|
|
17
|
+
'letterSpacing': 'letter-spacing',
|
|
18
|
+
'wordSpacing': 'word-spacing',
|
|
19
|
+
'color': 'color'
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const PropertyNameCSSMap: Record<string, string[]> = {
|
|
23
23
|
'transform': ['angle', 'scale'],
|
|
24
24
|
'border-radius': ['radius'],
|
|
25
25
|
'border-width': ['strokeWidth'],
|
|
26
|
-
'border-color': ['
|
|
27
|
-
'background': ['
|
|
26
|
+
'border-color': ['strokeColor'],
|
|
27
|
+
'background-color': ['fillColor'],
|
|
28
28
|
'filter': ['blur'],
|
|
29
29
|
'backdrop-filter': ['backdrop-blur'],
|
|
30
30
|
'letter-spacing': ['letterSpacing'],
|
|
31
31
|
'word-spacing': ['wordSpacing'],
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
|
|
34
35
|
export function getStyleKeysFromCSSProperty(cssProp: string): string[] {
|
|
35
36
|
const key = PropertyNameCSSMap[cssProp] ?? [cssProp];
|
|
36
37
|
return key;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
|
|
2
2
|
import { getTransition } from './getTransition';
|
|
3
3
|
import { getStyleKeysFromCSSProperty } from './CSSPropertyNameMap';
|
|
4
|
-
import {
|
|
4
|
+
import { InteractionItemTrigger } from '@cntrl-site/sdk';
|
|
5
5
|
|
|
6
6
|
export class ItemInteractionController implements ItemInteractionCtrl {
|
|
7
7
|
private transitionsInProgress: Set<string> = new Set();
|
|
@@ -15,12 +15,12 @@ export class ItemInteractionController implements ItemInteractionCtrl {
|
|
|
15
15
|
this.registry.register(itemId, this);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
getState
|
|
18
|
+
getState(keys: string[]) {
|
|
19
19
|
const stateProps = this.registry.getStatePropsForItem(this.itemId);
|
|
20
|
-
const styles = keys.reduce<Record<string,
|
|
20
|
+
const styles = keys.reduce<Record<string, string | number>>((map, styleKey) => {
|
|
21
21
|
const prop = stateProps[styleKey];
|
|
22
22
|
if (prop?.value === undefined) return map;
|
|
23
|
-
map[styleKey] = prop.value
|
|
23
|
+
map[styleKey] = prop.value;
|
|
24
24
|
return map;
|
|
25
25
|
}, {});
|
|
26
26
|
const transition = getTransition(stateProps, keys);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ArticleItemType,
|
|
1
|
+
import { ArticleItemType, InteractionItemTrigger, ItemState } from '@cntrl-site/sdk';
|
|
2
2
|
|
|
3
3
|
export interface ItemInteractionCtrl {
|
|
4
|
-
getState
|
|
4
|
+
getState(keys: string[]): StateCSSInfo;
|
|
5
5
|
getHasTrigger(itemId: string, triggerType: InteractionItemTrigger['type']): boolean;
|
|
6
6
|
sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
|
|
7
7
|
handleTransitionEnd?: (styleKey: string) => void;
|
|
@@ -13,19 +13,19 @@ export interface ItemInteractionCtrl {
|
|
|
13
13
|
|
|
14
14
|
export interface InteractionsRegistryPort {
|
|
15
15
|
register(itemId: string, ctrl: ItemInteractionCtrl): void;
|
|
16
|
-
getStatePropsForItem(itemId: string): StateProps
|
|
16
|
+
getStatePropsForItem(itemId: string): StateProps;
|
|
17
17
|
getItemAvailableTriggers(itemId: string): Set<InteractionItemTrigger['type']>;
|
|
18
18
|
notifyItemTrigger(itemId: string, type: TriggerType): void;
|
|
19
19
|
notifyTransitionEnd(itemId: string): void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
type StateCSSInfo
|
|
23
|
-
styles: Partial<Record<string,
|
|
22
|
+
type StateCSSInfo = {
|
|
23
|
+
styles: Partial<Record<string, string | number>>;
|
|
24
24
|
transition?: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
type StateProps
|
|
28
|
-
value?:
|
|
27
|
+
type StateProps = Record<keyof ItemState<ArticleItemType>, {
|
|
28
|
+
value?: string | number;
|
|
29
29
|
transition?: {
|
|
30
30
|
timing: string;
|
|
31
31
|
duration: number;
|
|
@@ -0,0 +1,66 @@
|
|
|
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 InteractionsContextOld = 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
|
+
<InteractionsContextOld.Provider value={{
|
|
51
|
+
transitionTo,
|
|
52
|
+
interactionsStatesMap,
|
|
53
|
+
interactions,
|
|
54
|
+
getItemTrigger
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</InteractionsContextOld.Provider>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type StatesMap = Record<InteractionId, StateId>;
|
|
63
|
+
type Trigger = { id: InteractionId, from: StateId, to: StateId };
|
|
64
|
+
type TriggerType = InteractionTrigger['type'];
|
|
65
|
+
type InteractionId = string;
|
|
66
|
+
type StateId = string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor } from '@testing-library/react';
|
|
3
|
+
import { InteractionsProvider, InteractionsContextOld } from './InteractionsContext.old';
|
|
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
|
+
<InteractionsContextOld.Consumer>
|
|
32
|
+
{value => {
|
|
33
|
+
contextValue = value;
|
|
34
|
+
return null;
|
|
35
|
+
}}
|
|
36
|
+
</InteractionsContextOld.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
|
+
<InteractionsContextOld.Consumer>
|
|
53
|
+
{value => {
|
|
54
|
+
contextValue = value;
|
|
55
|
+
return null;
|
|
56
|
+
}}
|
|
57
|
+
</InteractionsContextOld.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
|
+
<InteractionsContextOld.Consumer>
|
|
75
|
+
{value => {
|
|
76
|
+
contextValue = value;
|
|
77
|
+
return null;
|
|
78
|
+
}}
|
|
79
|
+
</InteractionsContextOld.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
|
+
});
|