@cntrl-site/sdk-nextjs 1.0.20 → 1.1.0

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 (49) hide show
  1. package/jest.config.js +2 -2
  2. package/lib/components/Article.js +9 -8
  3. package/lib/components/Head.js +1 -1
  4. package/lib/components/Item.js +39 -21
  5. package/lib/components/items/CodeEmbedItem.js +15 -12
  6. package/lib/components/items/CustomItem.js +7 -9
  7. package/lib/components/items/GroupItem.js +15 -12
  8. package/lib/components/items/ImageItem.js +28 -21
  9. package/lib/components/items/RectangleItem.js +28 -15
  10. package/lib/components/items/RichTextItem.js +33 -12
  11. package/lib/components/items/VideoItem.js +26 -19
  12. package/lib/components/items/VimeoEmbed.js +22 -20
  13. package/lib/components/items/YoutubeEmbed.js +20 -18
  14. package/lib/components/useItemPosition.js +10 -5
  15. package/lib/interactions/CSSPropertyNameMap.js +38 -0
  16. package/lib/interactions/InteractionsRegistry.js +220 -0
  17. package/lib/interactions/ItemInteractionCtrl.js +61 -0
  18. package/lib/interactions/getTransition.js +21 -0
  19. package/lib/interactions/types.js +2 -0
  20. package/lib/interactions/useItemInteractionCtrl.js +17 -0
  21. package/lib/provider/InteractionsContext.js +23 -0
  22. package/lib/utils/getStyleFromItemStateAndParams.js +9 -0
  23. package/package.json +7 -4
  24. package/src/components/Article.tsx +30 -27
  25. package/src/components/ArticleWrapper.tsx +1 -2
  26. package/src/components/Head.tsx +7 -11
  27. package/src/components/Item.tsx +75 -29
  28. package/src/components/items/CodeEmbedItem.tsx +15 -11
  29. package/src/components/items/CustomItem.tsx +9 -11
  30. package/src/components/items/GroupItem.tsx +17 -13
  31. package/src/components/items/ImageItem.tsx +35 -20
  32. package/src/components/items/RectangleItem.tsx +35 -14
  33. package/src/components/items/RichTextItem.tsx +41 -14
  34. package/src/components/items/VideoItem.tsx +34 -20
  35. package/src/components/items/VimeoEmbed.tsx +27 -19
  36. package/src/components/items/YoutubeEmbed.tsx +20 -18
  37. package/src/components/useItemPosition.ts +12 -5
  38. package/src/interactions/CSSPropertyNameMap.ts +38 -0
  39. package/src/interactions/InteractionsRegistry.ts +244 -0
  40. package/src/interactions/ItemInteractionCtrl.ts +62 -0
  41. package/src/interactions/getTransition.ts +27 -0
  42. package/src/interactions/types.ts +32 -0
  43. package/src/interactions/useItemInteractionCtrl.ts +18 -0
  44. package/src/provider/InteractionsContext.old.tsx +65 -0
  45. package/src/provider/InteractionsContext.test.tsx +97 -0
  46. package/src/provider/InteractionsContext.tsx +28 -0
  47. package/src/utils/getStyleFromItemStateAndParams.ts +8 -0
  48. package/lib/utils/HoverStyles/HoverStyles.js +0 -77
  49. package/src/utils/HoverStyles/HoverStyles.ts +0 -85
@@ -0,0 +1,244 @@
1
+ import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
+ import {
3
+ Article,
4
+ ArticleItemType,
5
+ Interaction,
6
+ InteractionTrigger,
7
+ ItemAny,
8
+ } from '@cntrl-site/sdk';
9
+ import { isItemType } from '../components/Item';
10
+
11
+ export class InteractionsRegistry implements InteractionsRegistryPort {
12
+ private ctrls: Map<ItemId, ItemInteractionCtrl> = new Map();
13
+ private items: ItemAny[];
14
+ private interactions: Interaction[];
15
+ private stateItemsIdsMap: StateItemsIdsMap;
16
+ private interactionStateMap: InteractionStateMap;
17
+ private itemsStages: ItemStages;
18
+ private activeStateIdInteractionIdMap: Record<StateId, InteractionId>;
19
+
20
+ constructor(article: Article, private layoutId: string) {
21
+ const { interactions } = article;
22
+ this.items = this.unpackItems(article);
23
+ const activeStatesIds = interactions.reduce<StateId[]>((map, inter) => {
24
+ const activeStateId = inter.states.find((state) => state.id !== inter.startStateId)?.id!;
25
+ map.push(activeStateId);
26
+ return map;
27
+ }, []);
28
+ const interactionStateMap = interactions.reduce<InteractionStateMap>((map, { id, startStateId }) => {
29
+ map[id] = startStateId;
30
+ return map;
31
+ }, {});
32
+ this.activeStateIdInteractionIdMap = interactions.reduce<Record<StateId, InteractionId>>((map, interaction) => {
33
+ const activeState = interaction.states.find((state) => state.id !== interaction.startStateId);
34
+ if (activeState) {
35
+ map[activeState.id] = interaction.id;
36
+ }
37
+ return map;
38
+ }, {});
39
+ const itemStages = this.getDefaultItemStages();
40
+ const stateItemsIdsMap = activeStatesIds.reduce<StateItemsIdsMap>((map, stateId) => {
41
+ map[stateId] = this.items
42
+ .filter((item) => {
43
+ const layoutStates = item.state[layoutId] ?? {};
44
+ const state = layoutStates?.[stateId] ?? {};
45
+ const hasKeys = Object.keys(state).length !== 0;
46
+ return hasKeys;
47
+ })
48
+ .map((item) => item.id);
49
+ return map;
50
+ }, {});
51
+ this.interactions = interactions;
52
+ this.itemsStages = itemStages;
53
+ this.stateItemsIdsMap = stateItemsIdsMap;
54
+ this.interactionStateMap = interactionStateMap;
55
+ }
56
+
57
+ register(itemId: ItemId, ctrl: ItemInteractionCtrl) {
58
+ this.ctrls.set(itemId, ctrl);
59
+ }
60
+
61
+ getStatePropsForItem(itemId: string) {
62
+ const { items, layoutId } = this;
63
+ const item = items.find((item) => item.id === itemId)!;
64
+ const itemStages = this.itemsStages.filter((stage) => stage.itemId === itemId);
65
+ itemStages.sort((a, b) => a.updated - b.updated);
66
+ const itemStyles: StateProps = {};
67
+ for (const stage of itemStages) {
68
+ if (stage.type === 'active') {
69
+ if (stage.isStartState) continue;
70
+ const params = item.state[layoutId]?.[stage.stateId!] ?? {};
71
+ for (const [key, stateDetails] of Object.entries(params)) {
72
+ itemStyles[key] = {
73
+ value: stateDetails.value
74
+ };
75
+ }
76
+ }
77
+ if (stage.type === 'transitioning') {
78
+ const activeStateId = stage.direction === 'in' ? stage.to : stage.from;
79
+ const params = item.state[layoutId]?.[activeStateId] ?? {};
80
+ for (const [key, stateDetails] of Object.entries(params)) {
81
+ itemStyles[key] = {
82
+ value: stage.direction === 'in' ? stateDetails.value : itemStyles[key]?.value,
83
+ transition: {
84
+ timing: stateDetails[stage.direction].timing,
85
+ duration: stateDetails[stage.direction].duration,
86
+ delay: stateDetails[stage.direction].delay
87
+ }
88
+ };
89
+ }
90
+ }
91
+ }
92
+ return itemStyles;
93
+ }
94
+
95
+ notifyTrigger(itemId: string, triggerType: TriggerType): void {
96
+ const timestamp = Date.now();
97
+ for (const interaction of this.interactions) {
98
+ const currentStateId = this.getCurrentStateByInteractionId(interaction.id);
99
+ const matchingTrigger = interaction.triggers.find((trigger) =>
100
+ trigger.itemId === itemId
101
+ && trigger.from === currentStateId
102
+ && trigger.type === triggerType
103
+ );
104
+ if (!matchingTrigger) continue;
105
+ const activeStateId = this.getActiveInteractionState(interaction.id);
106
+ const isNewStateActive = matchingTrigger.to === activeStateId;
107
+ this.setCurrentStateForInteraction(interaction.id, matchingTrigger.to);
108
+ const transitioningItems = this.stateItemsIdsMap[activeStateId] ?? [];
109
+ this.itemsStages = this.itemsStages.map((stage) => {
110
+ if (stage.interactionId !== interaction.id) return stage;
111
+ return {
112
+ itemId: stage.itemId,
113
+ interactionId: stage.interactionId,
114
+ type: 'transitioning',
115
+ from: stage.type === 'transitioning' ? stage.to : stage.stateId!,
116
+ to: matchingTrigger.to,
117
+ direction: isNewStateActive ? 'in' : 'out',
118
+ updated: timestamp
119
+ };
120
+ });
121
+ this.notifyItemCtrlsChange(transitioningItems);
122
+ this.notifyTransitionStartForItems(transitioningItems, activeStateId);
123
+ }
124
+ }
125
+
126
+ notifyTransitionStartForItems(itemsIds: string[], activeStateId: string) {
127
+ for (const itemId of itemsIds) {
128
+ const ctrl = this.ctrls.get(itemId);
129
+ const item = this.items.find((item) => item.id === itemId)!;
130
+ const keys = Object.keys(item.state[this.layoutId]?.[activeStateId] ?? {});
131
+ ctrl?.handleTransitionStart?.(keys);
132
+ }
133
+ }
134
+
135
+ notifyTransitionEnd(itemId: string): void {
136
+ const timestamp = Date.now();
137
+ this.itemsStages = this.itemsStages.map((stage) => {
138
+ if (stage.itemId !== itemId || stage.type !== 'transitioning') return stage;
139
+ return {
140
+ itemId: itemId,
141
+ interactionId: stage.interactionId,
142
+ type: 'active',
143
+ stateId: stage.to,
144
+ isStartState: stage.direction === 'out',
145
+ updated: timestamp
146
+ };
147
+ });
148
+ this.ctrls.get(itemId)?.receiveChange();
149
+ }
150
+
151
+
152
+ private getCurrentStateByInteractionId(id: InteractionId): string {
153
+ let state;
154
+ for (const interactionId of Object.keys(this.interactionStateMap)) {
155
+ if (id !== interactionId) continue;
156
+ state = this.interactionStateMap[interactionId];
157
+ }
158
+ if (!state) throw new Error(`Failed to find current state for interaction w/ id="${id}"`);
159
+ return state;
160
+ }
161
+
162
+ private setCurrentStateForInteraction(interactionId: InteractionId, stateId: StateId) {
163
+ this.interactionStateMap = {
164
+ ...this.interactionStateMap,
165
+ [interactionId]: stateId
166
+ };
167
+ }
168
+
169
+ private getActiveInteractionState(interactionId: InteractionId): string {
170
+ const { interactions } = this;
171
+ const interaction = interactions.find((interaction) => interaction.id === interactionId)!;
172
+ return interaction.states.find(state => state.id !== interaction.startStateId)?.id!;
173
+ }
174
+
175
+ private notifyItemCtrlsChange(itemsIds: string[]) {
176
+ for (const itemId of itemsIds) {
177
+ this.ctrls.get(itemId)?.receiveChange();
178
+ }
179
+ }
180
+
181
+ private unpackItems(article: Article): ItemAny[] {
182
+ const itemsArr = [];
183
+ for (const section of article.sections) {
184
+ for (const item of section.items) {
185
+ const { items, ...itemWithoutChildren } = item;
186
+ itemsArr.push(itemWithoutChildren);
187
+ if (!isItemType(item, ArticleItemType.Group)) continue;
188
+ const groupChildren = item?.items ?? [];
189
+ for (const child of groupChildren) {
190
+ itemsArr.push(child);
191
+ }
192
+ }
193
+ }
194
+ return itemsArr;
195
+ }
196
+
197
+ private getDefaultItemStages(): ItemStages {
198
+ const timestamp = Date.now();
199
+ const { items, layoutId } = this;
200
+ const stages: ItemStages = [];
201
+ for (const item of items) {
202
+ const itemStatesMap = item.state[layoutId];
203
+ if (!itemStatesMap) continue;
204
+ for (const stateId of Object.keys(itemStatesMap)) {
205
+ const interactionId = this.activeStateIdInteractionIdMap[stateId];
206
+ if (!interactionId) continue;
207
+ stages.push({
208
+ itemId: item.id,
209
+ interactionId,
210
+ type: 'active',
211
+ isStartState: true,
212
+ updated: timestamp
213
+ });
214
+ }
215
+ }
216
+ return stages;
217
+ }
218
+ }
219
+
220
+ type ItemStages = (TransitioningStage | ActiveStage)[];
221
+ type TransitioningStage = {
222
+ itemId: string;
223
+ interactionId: string;
224
+ type: 'transitioning';
225
+ from: StateId;
226
+ to: StateId;
227
+ direction: 'in' | 'out';
228
+ updated: number;
229
+ };
230
+ type ActiveStage = { type: 'active'; itemId: string; interactionId: string; stateId?: string; isStartState: boolean; updated: number; };
231
+ type InteractionStateMap = Record<InteractionId, StateId>;
232
+ type StateItemsIdsMap = Record<StateId, ItemId[]>;
233
+ type TriggerType = InteractionTrigger['type'];
234
+ type InteractionId = string;
235
+ type StateId = string;
236
+ type ItemId = string;
237
+ type StateProps = Record<string, {
238
+ value?: string | number;
239
+ transition?: {
240
+ timing: string;
241
+ duration: number;
242
+ delay: number;
243
+ };
244
+ }>;
@@ -0,0 +1,62 @@
1
+ import { InteractionsRegistryPort, ItemInteractionCtrl } from './types';
2
+ import { getTransition } from './getTransition';
3
+ import { getStyleKeysFromCSSProperty } from './CSSPropertyNameMap';
4
+
5
+ export class ItemInteractionController implements ItemInteractionCtrl {
6
+ private transitionsInProgress: Set<string> = new Set();
7
+ constructor(
8
+ private itemId: string,
9
+ private registry: InteractionsRegistryPort,
10
+ private onChange: () => void
11
+ ) {
12
+ this.registry.register(itemId, this);
13
+ }
14
+
15
+ getState(keys: string[]) {
16
+ const stateProps = this.registry.getStatePropsForItem(this.itemId);
17
+ const styles = keys.reduce<Record<string, string | number>>((map, styleKey) => {
18
+ const prop = stateProps[styleKey];
19
+ if (prop?.value === undefined) return map;
20
+ map[styleKey] = prop.value;
21
+ return map;
22
+ }, {});
23
+ const transition = getTransition(stateProps, keys);
24
+ return {
25
+ styles,
26
+ transition
27
+ };
28
+ }
29
+
30
+ sendTrigger(type: 'click' | 'hover-in' | 'hover-out') {
31
+ this.registry.notifyTrigger(this.itemId, type);
32
+ }
33
+
34
+ handleTransitionStart = (types: string[]) => {
35
+ this.transitionsInProgress.clear();
36
+ for (const type of types) {
37
+ this.transitionsInProgress.add(type);
38
+ }
39
+ };
40
+
41
+ handleTransitionEnd = (cssPropKey: string) => {
42
+ if (cssPropKey.startsWith('border-') && cssPropKey.endsWith('-radius')) {
43
+ cssPropKey = 'border-radius';
44
+ }
45
+ if (cssPropKey.startsWith('border-') && cssPropKey.endsWith('-width')) {
46
+ cssPropKey = 'border-width';
47
+ }
48
+ const styleKeys = getStyleKeysFromCSSProperty(cssPropKey);
49
+ for (const key of styleKeys) {
50
+ const found = this.transitionsInProgress.has(key);
51
+ if (!found) continue;
52
+ this.transitionsInProgress.delete(key);
53
+ break;
54
+ }
55
+ if (this.transitionsInProgress.size !== 0) return;
56
+ this.registry.notifyTransitionEnd(this.itemId);
57
+ };
58
+
59
+ receiveChange() {
60
+ this.onChange();
61
+ }
62
+ }
@@ -0,0 +1,27 @@
1
+ import { CSSPropertyNameMap } from './CSSPropertyNameMap';
2
+
3
+ export function getTransition(
4
+ state: Record<string, StateInfo>,
5
+ keys: string[]
6
+ ): string {
7
+ if (Object.keys(state).length === 0) return 'none';
8
+ const transitions = [];
9
+ for (const [key, params] of Object.entries(state)) {
10
+ if (!keys.includes(key) || !params.transition) continue;
11
+ const { transition: { duration, delay, timing } } = params;
12
+ const cssKey = CSSPropertyNameMap[key]!;
13
+ const nonZeroDuration = Math.max(duration, 0.01);
14
+ transitions.push(`${cssKey} ${nonZeroDuration}ms ${timing} ${delay}ms`);
15
+ }
16
+ return transitions.length > 0
17
+ ? transitions.join(', ')
18
+ : 'none';
19
+ }
20
+
21
+ type StateInfo = {
22
+ transition?: {
23
+ duration: number;
24
+ delay: number;
25
+ timing: string;
26
+ };
27
+ };
@@ -0,0 +1,32 @@
1
+ import { ArticleItemType, ItemState } from '@cntrl-site/sdk';
2
+
3
+ export interface ItemInteractionCtrl {
4
+ getState(keys: string[]): StateCSSInfo;
5
+ sendTrigger(type: 'click' | 'hover-in' | 'hover-out'): void;
6
+ handleTransitionEnd?: (styleKey: string) => void;
7
+ handleTransitionStart?: (styleKeys: string[]) => void;
8
+ receiveChange: () => void;
9
+ }
10
+
11
+ export interface InteractionsRegistryPort {
12
+ register(itemId: string, ctrl: ItemInteractionCtrl): void;
13
+ getStatePropsForItem(itemId: string): StateProps;
14
+ notifyTrigger(itemId: string, type: TriggerType): void;
15
+ notifyTransitionEnd(itemId: string): void;
16
+ }
17
+
18
+ type StateCSSInfo = {
19
+ styles: Partial<Record<string, string | number>>;
20
+ transition?: string;
21
+ };
22
+
23
+ type StateProps = Record<keyof ItemState<ArticleItemType>, {
24
+ value?: string | number;
25
+ transition?: {
26
+ timing: string;
27
+ duration: number;
28
+ delay: number;
29
+ };
30
+ }>;
31
+
32
+ type TriggerType = 'click' | 'hover-in' | 'hover-out';
@@ -0,0 +1,18 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { ItemInteractionController } from './ItemInteractionCtrl';
3
+ import { ItemInteractionCtrl } from './types';
4
+ import { useInteractionsRegistry } from '../provider/InteractionsContext';
5
+
6
+ export function useItemInteractionCtrl(itemId: string): ItemInteractionCtrl | undefined {
7
+ const [_, triggerRender] = useState(0);
8
+ const registry = useInteractionsRegistry();
9
+ const ctrl = useMemo(() => {
10
+ if (!registry) return;
11
+ return new ItemInteractionController(
12
+ itemId,
13
+ registry,
14
+ () => triggerRender(prev => prev + 1)
15
+ );
16
+ }, [itemId, registry]);
17
+ return ctrl;
18
+ }
@@ -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 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
+ {children}
57
+ </InteractionsContextOld.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;
@@ -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
+ });
@@ -0,0 +1,28 @@
1
+ import { createContext, FC, PropsWithChildren, useContext, useMemo } from 'react';
2
+ import { InteractionsRegistry } from '../interactions/InteractionsRegistry';
3
+ import { Article } from '@cntrl-site/sdk';
4
+ import { useCurrentLayout } from '../common/useCurrentLayout';
5
+
6
+ export const InteractionsContext = createContext<InteractionsRegistry | undefined>(undefined);
7
+
8
+ interface Props {
9
+ article: Article;
10
+ }
11
+
12
+ export const InteractionsProvider: FC<PropsWithChildren<Props>> = ({ article, children }) => {
13
+ const { layoutId } = useCurrentLayout();
14
+ const registry = useMemo(() => {
15
+ if (!layoutId) return;
16
+ return new InteractionsRegistry(article, layoutId);
17
+ }, [layoutId]);
18
+ return (
19
+ <InteractionsContext.Provider value={registry}>
20
+ {children}
21
+ </InteractionsContext.Provider>
22
+ );
23
+ };
24
+
25
+ export function useInteractionsRegistry(): InteractionsRegistry | undefined {
26
+ const registry = useContext(InteractionsContext);
27
+ return registry;
28
+ }
@@ -0,0 +1,8 @@
1
+ export function getStyleFromItemStateAndParams<T extends string | number>(
2
+ stateValue: string | number | undefined,
3
+ paramsValue: T | undefined
4
+ ): T | undefined {
5
+ return (stateValue as T) !== undefined
6
+ ? stateValue as T
7
+ : paramsValue;
8
+ }
@@ -1,77 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getHoverStyles = exports.getTransitions = void 0;
4
- const color_1 = require("@cntrl-site/color");
5
- const getItemTopStyle_1 = require("../getItemTopStyle");
6
- const hoverTransformationMap = {
7
- 'width': (width) => `width: ${width * 100}vw !important;`,
8
- 'height': (height) => `height: ${height * 100}vw !important;`,
9
- 'top': (top, anchorSide) => `top: ${(0, getItemTopStyle_1.getItemTopStyle)(top, anchorSide)} !important;`,
10
- 'left': (left) => `left: ${left * 100}vw !important;`,
11
- 'scale': (scale) => `transform: scale(${scale}) !important;`,
12
- 'angle': (angle) => `transform: rotate(${angle}deg) !important;`,
13
- 'opacity': (opacity) => `opacity: ${opacity} !important;`,
14
- 'radius': (radius) => `border-radius: ${radius * 100}vw !important;`,
15
- 'strokeWidth': (strokeWidth) => `border-width: ${strokeWidth * 100}vw !important;`,
16
- 'strokeColor': (strokeColor) => `border-color: ${color_1.CntrlColor.parse(strokeColor).toCss()} !important;`,
17
- 'fillColor': (fillColor) => `background-color: ${color_1.CntrlColor.parse(fillColor).toCss()} !important;`,
18
- 'blur': (blur) => `filter: blur(${blur * 100}vw) !important;`,
19
- 'backdropBlur': (backdropBlur) => `backdrop-filter: blur(${backdropBlur * 100}vw) !important;`,
20
- 'color': (color) => `color: ${color_1.CntrlColor.parse(color).toCss()} !important;`,
21
- 'letterSpacing': (letterSpacing) => `letter-spacing: ${letterSpacing * 100}vw !important;`,
22
- 'wordSpacing': (wordSpacing) => `word-spacing: ${wordSpacing * 100}vw !important;`
23
- };
24
- const CSSPropertyNameMap = {
25
- 'width': 'width',
26
- 'height': 'height',
27
- 'top': 'top',
28
- 'left': 'left',
29
- 'scale': 'transform',
30
- 'angle': 'transform',
31
- 'opacity': 'opacity',
32
- 'radius': 'border-radius',
33
- 'strokeWidth': 'border-width',
34
- 'strokeColor': 'border-color',
35
- 'fillColor': 'background-color',
36
- 'blur': 'filter',
37
- 'backdropBlur': 'backdrop-filter',
38
- 'letterSpacing': 'letter-spacing',
39
- 'wordSpacing': 'word-spacing',
40
- 'color': 'color'
41
- };
42
- function getTransitions(values, hover) {
43
- if (!hover)
44
- return 'unset';
45
- const transitionValues = values.reduce((acc, valueName) => {
46
- if (valueName in hover && hover[valueName] !== undefined) {
47
- const hoverProperties = hover[valueName];
48
- return [
49
- ...acc,
50
- `${CSSPropertyNameMap[valueName]} ${hoverProperties.duration}ms ${hoverProperties.timing} ${hoverProperties.delay}ms`
51
- ];
52
- }
53
- return acc;
54
- }, []);
55
- if (!transitionValues.length)
56
- return 'unset';
57
- return transitionValues.join(', ');
58
- }
59
- exports.getTransitions = getTransitions;
60
- function getHoverStyles(values, hover, anchorSide) {
61
- if (!hover)
62
- return '';
63
- const hoverValues = values.reduce((acc, valueName) => {
64
- if (valueName in hover && hover[valueName] !== undefined) {
65
- const hoverProperties = hover[valueName];
66
- return [
67
- ...acc,
68
- hoverTransformationMap[valueName](hoverProperties.value, anchorSide)
69
- ];
70
- }
71
- return acc;
72
- }, []);
73
- if (!hoverValues.length)
74
- return '';
75
- return hoverValues.join('\n');
76
- }
77
- exports.getHoverStyles = getHoverStyles;