overlapping-cards-scroll 0.1.6 → 0.1.7

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.
@@ -27,6 +27,7 @@ import type {
27
27
  OverlappingCardsScrollRNTabsContainerProps,
28
28
  OverlappingCardsScrollRNTabProps,
29
29
  OverlappingCardsScrollRNTabsPosition,
30
+ OverlappingCardsScrollRNTabsAlign,
30
31
  } from "./OverlappingCardsScrollRN.types";
31
32
 
32
33
  export type {
@@ -37,6 +38,7 @@ export type {
37
38
  OverlappingCardsScrollRNPageDotsPosition,
38
39
  OverlappingCardsScrollRNProps,
39
40
  OverlappingCardsScrollRNSnapDecelerationRate,
41
+ OverlappingCardsScrollRNTabsAlign,
40
42
  OverlappingCardsScrollRNTabsContainerProps,
41
43
  OverlappingCardsScrollRNTabProps,
42
44
  OverlappingCardsScrollRNTabsPosition,
@@ -44,14 +46,37 @@ export type {
44
46
 
45
47
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
46
48
  const PAGE_DOT_POSITIONS = new Set(["above", "below", "overlay"]);
47
- const TAB_POSITIONS = new Set(["above", "below"]);
49
+ interface ParsedTabsPosition {
50
+ side: "top" | "bottom" | "left" | "right";
51
+ align: "start" | "center" | "end";
52
+ orientation: "horizontal" | "vertical";
53
+ }
54
+
55
+ const TABS_POSITION_MAP: Record<string, ParsedTabsPosition> = {
56
+ "top-left": { side: "top", align: "start", orientation: "horizontal" },
57
+ "top-center": { side: "top", align: "center", orientation: "horizontal" },
58
+ "top-right": { side: "top", align: "end", orientation: "horizontal" },
59
+ "bottom-left": { side: "bottom", align: "start", orientation: "horizontal" },
60
+ "bottom-center": { side: "bottom", align: "center", orientation: "horizontal" },
61
+ "bottom-right": { side: "bottom", align: "end", orientation: "horizontal" },
62
+ "left-top": { side: "left", align: "start", orientation: "vertical" },
63
+ "left-center": { side: "left", align: "center", orientation: "vertical" },
64
+ "left-bottom": { side: "left", align: "end", orientation: "vertical" },
65
+ "right-top": { side: "right", align: "start", orientation: "vertical" },
66
+ "right-center": { side: "right", align: "center", orientation: "vertical" },
67
+ "right-bottom": { side: "right", align: "end", orientation: "vertical" },
68
+ "above": { side: "top", align: "center", orientation: "horizontal" },
69
+ "below": { side: "bottom", align: "center", orientation: "horizontal" },
70
+ };
71
+
72
+ const DEFAULT_TABS_POSITION: ParsedTabsPosition = { side: "top", align: "center", orientation: "horizontal" };
73
+
74
+ const parseTabsPosition = (value: string | undefined): ParsedTabsPosition =>
75
+ (value && TABS_POSITION_MAP[value]) || DEFAULT_TABS_POSITION;
48
76
 
49
77
  const normalizePageDotsPosition = (value) =>
50
78
  PAGE_DOT_POSITIONS.has(value) ? value : "below";
51
79
 
52
- const normalizeTabsPosition = (value) =>
53
- TAB_POSITIONS.has(value) ? value : "above";
54
-
55
80
  const toNumericOffset = (value, fallback = 0) => {
56
81
  if (typeof value === "number" && Number.isFinite(value)) {
57
82
  return value;
@@ -319,7 +344,8 @@ export function OverlappingCardsScrollRN(props: OverlappingCardsScrollRNProps) {
319
344
  }, [itemsProp]);
320
345
 
321
346
  const cardCount = cards.length;
322
- const resolvedTabsPosition = normalizeTabsPosition(tabsPosition);
347
+ const parsedTabsPosition = parseTabsPosition(tabsPosition);
348
+ const isVerticalTabs = parsedTabsPosition.orientation === "vertical";
323
349
  const showNavigationTabs = showTabs && cardCount > 1 && cardNames !== null;
324
350
  const resolvedPageDotsOffset = toNumericOffset(pageDotsOffset, 10);
325
351
  const resolvedTabsOffset = toNumericOffset(tabsOffset, 10);
@@ -677,24 +703,33 @@ export function OverlappingCardsScrollRN(props: OverlappingCardsScrollRNProps) {
677
703
  );
678
704
  };
679
705
 
680
- const renderTabs = (position: OverlappingCardsScrollRNTabsPosition) => {
681
- if (
682
- !showNavigationTabs ||
683
- resolvedTabsPosition !== position ||
684
- cardNames === null
685
- ) {
706
+ const renderTabs = () => {
707
+ if (!showNavigationTabs || cardNames === null) {
686
708
  return null;
687
709
  }
688
710
 
711
+ const { side, align, orientation } = parsedTabsPosition;
712
+ const isVertical = orientation === "vertical";
713
+
714
+ const justifyContent: "flex-start" | "flex-end" | "center" =
715
+ align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center";
716
+
717
+ const baseStyle = isVertical ? styles.tabsColumn : styles.tabsRow;
718
+
689
719
  const containerStyle =
690
- position === "above"
691
- ? [styles.tabsRow, { marginBottom: resolvedTabsOffset }]
692
- : [styles.tabsRow, { marginTop: resolvedTabsOffset }];
720
+ side === "top"
721
+ ? [baseStyle, { justifyContent, marginBottom: resolvedTabsOffset }]
722
+ : side === "bottom"
723
+ ? [baseStyle, { justifyContent, marginTop: resolvedTabsOffset }]
724
+ : side === "left"
725
+ ? [baseStyle, { justifyContent, marginRight: resolvedTabsOffset }]
726
+ : [baseStyle, { justifyContent, marginLeft: resolvedTabsOffset }];
693
727
 
694
728
  return (
695
729
  <TabsContainerComponent
696
- position={position}
697
- className={`rn-ocs-tabs rn-ocs-tabs--${position}`}
730
+ position={side}
731
+ align={align}
732
+ className={`rn-ocs-tabs rn-ocs-tabs--${side}`}
698
733
  style={containerStyle}
699
734
  ariaLabel="Card tabs"
700
735
  cardNames={cardNames}
@@ -715,10 +750,11 @@ export function OverlappingCardsScrollRN(props: OverlappingCardsScrollRNProps) {
715
750
 
716
751
  return (
717
752
  <TabsComponent
718
- key={`rn-ocs-tab-${position}-${index}`}
753
+ key={`rn-ocs-tab-${side}-${index}`}
719
754
  name={name}
720
755
  index={index}
721
- position={position}
756
+ position={side}
757
+ align={align}
722
758
  isPrincipal={isPrincipal}
723
759
  influence={influence}
724
760
  animate={animate}
@@ -740,128 +776,144 @@ export function OverlappingCardsScrollRN(props: OverlappingCardsScrollRNProps) {
740
776
  );
741
777
  };
742
778
 
743
- return (
744
- <OverlappingCardsScrollRNControllerContext.Provider
745
- value={controllerContextValue}
746
- >
747
- <View style={[styles.shell, style]}>
748
- {renderTabs("above")}
749
- {renderPageDots("above")}
750
- <View
751
- style={[styles.root, { height: resolvedCardHeight }]}
752
- onLayout={(event) => {
753
- const width = event.nativeEvent.layout.width || 1;
754
- setViewportWidth(Math.max(1, width));
779
+ const tabsBeforeStage =
780
+ parsedTabsPosition.side === "top" || parsedTabsPosition.side === "left";
781
+
782
+ // The page dots + card area content (reused for both layouts)
783
+ const stageAndDots = (
784
+ <>
785
+ {renderPageDots("above")}
786
+ <View
787
+ style={[styles.root, { height: resolvedCardHeight }]}
788
+ onLayout={(event) => {
789
+ const width = event.nativeEvent.layout.width || 1;
790
+ setViewportWidth(Math.max(1, width));
791
+ }}
792
+ >
793
+ <Animated.ScrollView
794
+ ref={scrollRef}
795
+ horizontal
796
+ style={[styles.scrollRegion, { height: resolvedCardHeight }]}
797
+ contentContainerStyle={{
798
+ width: layout.trackWidth,
799
+ height: resolvedCardHeight,
755
800
  }}
801
+ onScroll={onScroll}
802
+ onScrollBeginDrag={cancelFocusTransition}
803
+ onMomentumScrollBegin={cancelFocusTransition}
804
+ scrollEventThrottle={16}
805
+ showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
806
+ snapToInterval={shouldSnapToCard ? layout.stepDistance : undefined}
807
+ snapToAlignment={shouldSnapToCard ? "start" : undefined}
808
+ decelerationRate={
809
+ shouldSnapToCard
810
+ ? (snapDecelerationRate as number | "normal" | "fast")
811
+ : "normal"
812
+ }
813
+ disableIntervalMomentum={
814
+ shouldSnapToCard ? snapDisableIntervalMomentum : false
815
+ }
756
816
  >
757
- <Animated.ScrollView
758
- ref={scrollRef}
759
- horizontal
760
- style={[styles.scrollRegion, { height: resolvedCardHeight }]}
761
- contentContainerStyle={{
762
- width: layout.trackWidth,
763
- height: resolvedCardHeight,
764
- }}
765
- onScroll={onScroll}
766
- onScrollBeginDrag={cancelFocusTransition}
767
- onMomentumScrollBegin={cancelFocusTransition}
768
- scrollEventThrottle={16}
769
- showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
770
- snapToInterval={shouldSnapToCard ? layout.stepDistance : undefined}
771
- snapToAlignment={shouldSnapToCard ? "start" : undefined}
772
- decelerationRate={
773
- shouldSnapToCard
774
- ? (snapDecelerationRate as number | "normal" | "fast")
775
- : "normal"
776
- }
777
- disableIntervalMomentum={
778
- shouldSnapToCard ? snapDisableIntervalMomentum : false
779
- }
817
+ <View
818
+ style={[
819
+ styles.track,
820
+ { width: layout.trackWidth, height: resolvedCardHeight },
821
+ ]}
780
822
  >
781
- <View
782
- style={[
783
- styles.track,
784
- { width: layout.trackWidth, height: resolvedCardHeight },
785
- ]}
786
- >
787
- {cards.map((card, index) => {
788
- const restingRightX =
789
- index === 0
790
- ? 0
791
- : (index - 1) * layout.peek + layout.cardWidth;
792
- const restingLeftX = index * layout.peek;
793
-
794
- const cardXDuringNormalScroll =
795
- index === 0
796
- ? 0
797
- : scrollX.interpolate({
798
- inputRange:
799
- index === 1
800
- ? [0, layout.stepDistance]
801
- : [
802
- (index - 1) * layout.stepDistance,
803
- index * layout.stepDistance,
804
- ],
805
- outputRange: [restingRightX, restingLeftX],
806
- extrapolate: "clamp",
807
- });
808
-
809
- const cardXDuringFocusTransition = focusTransition
810
- ? focusTransitionProgress.interpolate({
811
- inputRange: [0, 1],
812
- outputRange: [
813
- resolveCardXAtProgress(
814
- index,
815
- focusTransition.fromProgress,
816
- layout,
817
- ),
818
- resolveCardXAtProgress(
819
- index,
820
- focusTransition.toProgress,
821
- layout,
822
- ),
823
- ],
823
+ {cards.map((card, index) => {
824
+ const restingRightX =
825
+ index === 0
826
+ ? 0
827
+ : (index - 1) * layout.peek + layout.cardWidth;
828
+ const restingLeftX = index * layout.peek;
829
+
830
+ const cardXDuringNormalScroll =
831
+ index === 0
832
+ ? 0
833
+ : scrollX.interpolate({
834
+ inputRange:
835
+ index === 1
836
+ ? [0, layout.stepDistance]
837
+ : [
838
+ (index - 1) * layout.stepDistance,
839
+ index * layout.stepDistance,
840
+ ],
841
+ outputRange: [restingRightX, restingLeftX],
824
842
  extrapolate: "clamp",
825
- })
826
- : null;
827
-
828
- const animatedCardX =
829
- cardXDuringFocusTransition ?? cardXDuringNormalScroll;
830
-
831
- return (
832
- <Animated.View
833
- key={card.key ?? `rn-ocs-card-${index}`}
834
- pointerEvents="box-none"
835
- style={[
836
- styles.card,
837
- {
838
- width: layout.cardWidth,
839
- height: resolvedCardHeight,
840
- transform: [
841
- {
842
- translateX: Animated.add(scrollX, animatedCardX),
843
- },
844
- ],
845
- },
846
- cardContainerStyle,
847
- ]}
848
- >
849
- <View pointerEvents="auto" style={styles.cardContent}>
850
- <OverlappingCardsScrollRNCardIndexContext.Provider
851
- value={index}
852
- >
853
- {card}
854
- </OverlappingCardsScrollRNCardIndexContext.Provider>
855
- </View>
856
- </Animated.View>
857
- );
858
- })}
859
- </View>
860
- </Animated.ScrollView>
861
- {renderPageDots("overlay")}
862
- </View>
863
- {renderPageDots("below")}
864
- {renderTabs("below")}
843
+ });
844
+
845
+ const cardXDuringFocusTransition = focusTransition
846
+ ? focusTransitionProgress.interpolate({
847
+ inputRange: [0, 1],
848
+ outputRange: [
849
+ resolveCardXAtProgress(
850
+ index,
851
+ focusTransition.fromProgress,
852
+ layout,
853
+ ),
854
+ resolveCardXAtProgress(
855
+ index,
856
+ focusTransition.toProgress,
857
+ layout,
858
+ ),
859
+ ],
860
+ extrapolate: "clamp",
861
+ })
862
+ : null;
863
+
864
+ const animatedCardX =
865
+ cardXDuringFocusTransition ?? cardXDuringNormalScroll;
866
+
867
+ return (
868
+ <Animated.View
869
+ key={card.key ?? `rn-ocs-card-${index}`}
870
+ pointerEvents="box-none"
871
+ style={[
872
+ styles.card,
873
+ {
874
+ width: layout.cardWidth,
875
+ height: resolvedCardHeight,
876
+ transform: [
877
+ {
878
+ translateX: Animated.add(scrollX, animatedCardX),
879
+ },
880
+ ],
881
+ },
882
+ cardContainerStyle,
883
+ ]}
884
+ >
885
+ <View pointerEvents="auto" style={styles.cardContent}>
886
+ <OverlappingCardsScrollRNCardIndexContext.Provider
887
+ value={index}
888
+ >
889
+ {card}
890
+ </OverlappingCardsScrollRNCardIndexContext.Provider>
891
+ </View>
892
+ </Animated.View>
893
+ );
894
+ })}
895
+ </View>
896
+ </Animated.ScrollView>
897
+ {renderPageDots("overlay")}
898
+ </View>
899
+ {renderPageDots("below")}
900
+ </>
901
+ );
902
+
903
+ const stageContent = isVerticalTabs ? (
904
+ <View style={styles.mainColumn}>{stageAndDots}</View>
905
+ ) : (
906
+ stageAndDots
907
+ );
908
+
909
+ return (
910
+ <OverlappingCardsScrollRNControllerContext.Provider
911
+ value={controllerContextValue}
912
+ >
913
+ <View style={[isVerticalTabs ? styles.shellRow : styles.shell, style]}>
914
+ {tabsBeforeStage ? renderTabs() : null}
915
+ {stageContent}
916
+ {!tabsBeforeStage ? renderTabs() : null}
865
917
  </View>
866
918
  </OverlappingCardsScrollRNControllerContext.Provider>
867
919
  );
@@ -872,6 +924,15 @@ const styles = StyleSheet.create({
872
924
  width: "100%",
873
925
  minWidth: 0,
874
926
  },
927
+ shellRow: {
928
+ width: "100%",
929
+ minWidth: 0,
930
+ flexDirection: "row",
931
+ },
932
+ mainColumn: {
933
+ flex: 1,
934
+ minWidth: 0,
935
+ },
875
936
  root: {
876
937
  width: "100%",
877
938
  minWidth: 0,
@@ -927,6 +988,12 @@ const styles = StyleSheet.create({
927
988
  flexWrap: "wrap",
928
989
  zIndex: 6,
929
990
  },
991
+ tabsColumn: {
992
+ flexDirection: "column",
993
+ alignItems: "center",
994
+ justifyContent: "center",
995
+ zIndex: 6,
996
+ },
930
997
  tab: {
931
998
  borderRadius: 999,
932
999
  borderWidth: 1,
@@ -37,12 +37,15 @@ export type OverlappingCardsScrollRNSnapDecelerationRate =
37
37
 
38
38
  export type OverlappingCardsScrollRNItem = OverlappingCardsScrollWebCardItem;
39
39
 
40
- export type OverlappingCardsScrollRNTabsPosition = "above" | "below";
40
+ export type OverlappingCardsScrollRNTabsPosition = "top" | "bottom" | "left" | "right";
41
+
42
+ export type OverlappingCardsScrollRNTabsAlign = "start" | "center" | "end";
41
43
 
42
44
  export interface OverlappingCardsScrollRNTabProps {
43
45
  name: string;
44
46
  index: number;
45
47
  position: OverlappingCardsScrollRNTabsPosition;
48
+ align: OverlappingCardsScrollRNTabsAlign;
46
49
  isPrincipal: boolean;
47
50
  influence: number;
48
51
  animate: {
@@ -64,6 +67,7 @@ export interface OverlappingCardsScrollRNTabProps {
64
67
  export interface OverlappingCardsScrollRNTabsContainerProps {
65
68
  children: ReactNode;
66
69
  position: OverlappingCardsScrollRNTabsPosition;
70
+ align: OverlappingCardsScrollRNTabsAlign;
67
71
  className: string;
68
72
  style: StyleProp<ViewStyle>;
69
73
  ariaLabel: string;