@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.
Files changed (39) hide show
  1. package/lib/components/items/FileItem/ImageItem.js +22 -46
  2. package/lib/components/items/FileItem/VideoItem.js +21 -45
  3. package/lib/components/items/FileItem/useFileItem.js +5 -12
  4. package/lib/components/items/RectangleItem/RectangleItem.js +22 -78
  5. package/lib/components/items/RectangleItem/useRectangleItem.js +8 -15
  6. package/lib/interactions/CSSPropertyNameMap.js +18 -18
  7. package/lib/utils/Animator/Animator.js +19 -171
  8. package/package.json +2 -2
  9. package/src/components/items/CodeEmbedItem/CodeEmbedItem.tsx +1 -1
  10. package/src/components/items/ComponentItem/ComponentItem.tsx +1 -1
  11. package/src/components/items/CompoundItem/CompoundChild.tsx +2 -2
  12. package/src/components/items/CompoundItem/CompoundItem.tsx +1 -1
  13. package/src/components/items/CustomItem/CustomItem.tsx +1 -1
  14. package/src/components/items/EmbedVideoItem/VimeoEmbed.tsx +2 -2
  15. package/src/components/items/EmbedVideoItem/YoutubeEmbed.tsx +2 -2
  16. package/src/components/items/FileItem/ImageItem.tsx +11 -60
  17. package/src/components/items/FileItem/VideoItem.tsx +12 -61
  18. package/src/components/items/FileItem/useFileItem.ts +6 -14
  19. package/src/components/items/GroupItem/GroupItem.tsx +1 -1
  20. package/src/components/items/Item.tsx +2 -2
  21. package/src/components/items/RectangleItem/RectangleItem.tsx +24 -126
  22. package/src/components/items/RectangleItem/useRectangleItem.ts +10 -17
  23. package/src/components/items/RichTextItem/RichTextItem.tsx +5 -5
  24. package/src/interactions/CSSPropertyNameMap.ts +19 -18
  25. package/src/interactions/ItemInteractionCtrl.ts +4 -4
  26. package/src/interactions/types.ts +7 -7
  27. package/src/provider/InteractionsContext.old.tsx +66 -0
  28. package/src/provider/InteractionsContext.test.tsx +97 -0
  29. package/src/utils/Animator/Animator.ts +27 -227
  30. package/src/utils/getStyleFromItemStateAndParams.ts +2 -2
  31. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  32. package/.idea/modules.xml +0 -8
  33. package/.idea/sdk-nextjs.iml +0 -12
  34. package/.idea/vcs.xml +0 -6
  35. package/lib/utils/areFillsVisible/areFillsVisible.js +0 -23
  36. package/lib/utils/getFill.js +0 -43
  37. package/src/utils/areFillsVisible/areFillsVisible.test.ts +0 -63
  38. package/src/utils/areFillsVisible/areFillsVisible.ts +0 -37
  39. 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
- fill: itemFill,
17
+ fillColor: itemFillColor,
21
18
  radius: itemRadius,
22
19
  strokeWidth: itemStrokeWidth,
23
- strokeFill: itemStrokeFill,
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<number>(['angle', 'strokeWidth', 'radius', 'blur', 'backdropBlur']);
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 = areFillsVisible(stateFillLayers ?? itemFill ?? []) || (strokeWidth !== 0 && areFillsVisible(stateStrokeFillLayers ?? itemStrokeFill ?? []));
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
- ...(strokeValue ? {
65
- '--stroke-background': stroke,
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
- return (`
79
+ return (`
141
80
  .rectangle-${item.id} {
142
- border-radius: ${layoutParams.radius * 100}vw
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 defaultFill = [
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 fill = useKeyframeValue(
33
+ const fillColor = useKeyframeValue(
41
34
  item,
42
- KeyframeType.Fill,
35
+ KeyframeType.Color,
43
36
  (item, layoutId) => {
44
37
  if (!layoutId) return;
45
38
  const layoutParams = item.layoutParams[layoutId];
46
- return 'fill' in layoutParams ? layoutParams.fill : defaultFill;
39
+ return 'fillColor' in layoutParams ? layoutParams.fillColor : defaultColor;
47
40
  },
48
- (animator, scroll, value) => value ? animator.getFill(value, scroll) : undefined,
41
+ (animator, scroll, value) => value ? animator.getColor({ color: value }, scroll).color : undefined,
49
42
  sectionId,
50
43
  [layoutId]
51
44
  );
52
- const strokeFill = useKeyframeValue(
45
+ const strokeColor = useKeyframeValue(
53
46
  item,
54
- KeyframeType.BorderFill,
47
+ KeyframeType.BorderColor,
55
48
  (item, layoutId) => {
56
49
  if (!layoutId) return;
57
50
  const layoutParams = item.layoutParams[layoutId];
58
- return 'strokeFill' in layoutParams ? layoutParams.strokeFill : defaultFill;
51
+ return 'strokeColor' in layoutParams ? layoutParams.strokeColor : defaultColor;
59
52
  },
60
- (animator, scroll, value) => value ? animator.getBorderFill(value, scroll) : undefined,
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 { fill, strokeWidth, radius, strokeFill, blur, backdropBlur };
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<number | string>(['angle', 'blur', 'letterSpacing', 'wordSpacing', 'color']);
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 as string, itemColor);
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 as number * 100}vw)` } : {}),
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 as number * exemplary}px` } : {}),
67
- ...(wordSpacing !== undefined ? { wordSpacing: `${wordSpacing as number * exemplary}px` } : {}),
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
- strokeFill: 'border-color',
14
- fill: 'background',
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': ['strokeFill'],
27
- 'background': ['fill'],
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 { FillLayer, InteractionItemTrigger } from '@cntrl-site/sdk';
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<T>(keys: string[]) {
18
+ getState(keys: string[]) {
19
19
  const stateProps = this.registry.getStatePropsForItem(this.itemId);
20
- const styles = keys.reduce<Record<string, T>>((map, styleKey) => {
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 as T;
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, FillLayer, InteractionItemTrigger, ItemState } from '@cntrl-site/sdk';
1
+ import { ArticleItemType, InteractionItemTrigger, ItemState } from '@cntrl-site/sdk';
2
2
 
3
3
  export interface ItemInteractionCtrl {
4
- getState<T>(keys: string[]): StateCSSInfo<T>;
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<unknown>;
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<T> = {
23
- styles: Partial<Record<string, T>>;
22
+ type StateCSSInfo = {
23
+ styles: Partial<Record<string, string | number>>;
24
24
  transition?: string;
25
25
  };
26
26
 
27
- type StateProps<T> = Record<keyof ItemState<ArticleItemType>, {
28
- value?: T;
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
+ });