@teambit/react.ui.component-highlighter 0.0.0-4d64e75088ad738b699f1df7db0bcd7201d589d0

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 (134) hide show
  1. package/children-highlighter/children-highlighter.composition.tsx +103 -0
  2. package/children-highlighter/children-highlighter.spec.tsx +22 -0
  3. package/children-highlighter/children-highlighter.tsx +9 -0
  4. package/children-highlighter/index.ts +5 -0
  5. package/children-highlighter/use-children-highlighter.tsx +79 -0
  6. package/component-highlighter.docs.md +191 -0
  7. package/dist/children-highlighter/children-highlighter.composition.d.ts +6 -0
  8. package/dist/children-highlighter/children-highlighter.composition.js +93 -0
  9. package/dist/children-highlighter/children-highlighter.composition.js.map +1 -0
  10. package/dist/children-highlighter/children-highlighter.d.ts +4 -0
  11. package/dist/children-highlighter/children-highlighter.js +24 -0
  12. package/dist/children-highlighter/children-highlighter.js.map +1 -0
  13. package/dist/children-highlighter/children-highlighter.spec.d.ts +1 -0
  14. package/dist/children-highlighter/children-highlighter.spec.js +22 -0
  15. package/dist/children-highlighter/children-highlighter.spec.js.map +1 -0
  16. package/dist/children-highlighter/index.d.ts +4 -0
  17. package/dist/children-highlighter/index.js +8 -0
  18. package/dist/children-highlighter/index.js.map +1 -0
  19. package/dist/children-highlighter/use-children-highlighter.d.ts +18 -0
  20. package/dist/children-highlighter/use-children-highlighter.js +51 -0
  21. package/dist/children-highlighter/use-children-highlighter.js.map +1 -0
  22. package/dist/component-highlighter.docs.md +191 -0
  23. package/dist/element-highlighter/element-highlighter.compositions.d.ts +14 -0
  24. package/dist/element-highlighter/element-highlighter.compositions.js +103 -0
  25. package/dist/element-highlighter/element-highlighter.compositions.js.map +1 -0
  26. package/dist/element-highlighter/element-highlighter.d.ts +22 -0
  27. package/dist/element-highlighter/element-highlighter.js +31 -0
  28. package/dist/element-highlighter/element-highlighter.js.map +1 -0
  29. package/dist/element-highlighter/element-highlighter.module.scss +10 -0
  30. package/dist/element-highlighter/index.d.ts +2 -0
  31. package/dist/element-highlighter/index.js +6 -0
  32. package/dist/element-highlighter/index.js.map +1 -0
  33. package/dist/frame/frame.d.ts +13 -0
  34. package/dist/frame/frame.js +126 -0
  35. package/dist/frame/frame.js.map +1 -0
  36. package/dist/frame/frame.module.scss +23 -0
  37. package/dist/frame/index.d.ts +2 -0
  38. package/dist/frame/index.js +6 -0
  39. package/dist/frame/index.js.map +1 -0
  40. package/dist/hover-highlighter/bubble-to-component.d.ts +24 -0
  41. package/dist/hover-highlighter/bubble-to-component.js +55 -0
  42. package/dist/hover-highlighter/bubble-to-component.js.map +1 -0
  43. package/dist/hover-highlighter/bubble-to-component.spec.d.ts +1 -0
  44. package/dist/hover-highlighter/bubble-to-component.spec.js +38 -0
  45. package/dist/hover-highlighter/bubble-to-component.spec.js.map +1 -0
  46. package/dist/hover-highlighter/hover-highlighter.compositions.d.ts +4 -0
  47. package/dist/hover-highlighter/hover-highlighter.compositions.js +73 -0
  48. package/dist/hover-highlighter/hover-highlighter.compositions.js.map +1 -0
  49. package/dist/hover-highlighter/hover-highlighter.d.ts +4 -0
  50. package/dist/hover-highlighter/hover-highlighter.js +24 -0
  51. package/dist/hover-highlighter/hover-highlighter.js.map +1 -0
  52. package/dist/hover-highlighter/hover-highlighter.spec.d.ts +1 -0
  53. package/dist/hover-highlighter/hover-highlighter.spec.js +95 -0
  54. package/dist/hover-highlighter/hover-highlighter.spec.js.map +1 -0
  55. package/dist/hover-highlighter/index.d.ts +4 -0
  56. package/dist/hover-highlighter/index.js +8 -0
  57. package/dist/hover-highlighter/index.js.map +1 -0
  58. package/dist/hover-highlighter/use-hover-highlighter.d.ts +25 -0
  59. package/dist/hover-highlighter/use-hover-highlighter.js +47 -0
  60. package/dist/hover-highlighter/use-hover-highlighter.js.map +1 -0
  61. package/dist/hybrid-highlighter/hybrid-highlighter.d.ts +36 -0
  62. package/dist/hybrid-highlighter/hybrid-highlighter.js +79 -0
  63. package/dist/hybrid-highlighter/hybrid-highlighter.js.map +1 -0
  64. package/dist/hybrid-highlighter/index.d.ts +2 -0
  65. package/dist/hybrid-highlighter/index.js +6 -0
  66. package/dist/hybrid-highlighter/index.js.map +1 -0
  67. package/dist/ignore-highlighter.d.ts +19 -0
  68. package/dist/ignore-highlighter.js +25 -0
  69. package/dist/ignore-highlighter.js.map +1 -0
  70. package/dist/index.d.ts +10 -0
  71. package/dist/index.js +18 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/label/component-strip.compositions.d.ts +2 -0
  74. package/dist/label/component-strip.compositions.js +17 -0
  75. package/dist/label/component-strip.compositions.js.map +1 -0
  76. package/dist/label/component-strip.d.ts +7 -0
  77. package/dist/label/component-strip.js +61 -0
  78. package/dist/label/component-strip.js.map +1 -0
  79. package/dist/label/component-strip.module.scss +68 -0
  80. package/dist/label/index.d.ts +4 -0
  81. package/dist/label/index.js +8 -0
  82. package/dist/label/index.js.map +1 -0
  83. package/dist/label/label-container.d.ts +12 -0
  84. package/dist/label/label-container.js +75 -0
  85. package/dist/label/label-container.js.map +1 -0
  86. package/dist/label/label.d.ts +6 -0
  87. package/dist/label/label.js +60 -0
  88. package/dist/label/label.js.map +1 -0
  89. package/dist/label/label.module.scss +32 -0
  90. package/dist/label/links.d.ts +2 -0
  91. package/dist/label/links.js +16 -0
  92. package/dist/label/links.js.map +1 -0
  93. package/dist/label/other-components.d.ts +9 -0
  94. package/dist/label/other-components.js +34 -0
  95. package/dist/label/other-components.js.map +1 -0
  96. package/dist/mock-component.d.ts +14 -0
  97. package/dist/mock-component.js +43 -0
  98. package/dist/mock-component.js.map +1 -0
  99. package/dist/preview-1751963029152.js +10 -0
  100. package/dist/rule-matcher.d.ts +8 -0
  101. package/dist/rule-matcher.js +32 -0
  102. package/dist/rule-matcher.js.map +1 -0
  103. package/element-highlighter/element-highlighter.compositions.tsx +130 -0
  104. package/element-highlighter/element-highlighter.module.scss +10 -0
  105. package/element-highlighter/element-highlighter.tsx +51 -0
  106. package/element-highlighter/index.ts +2 -0
  107. package/frame/frame.module.scss +23 -0
  108. package/frame/frame.tsx +139 -0
  109. package/frame/index.ts +2 -0
  110. package/hover-highlighter/bubble-to-component.spec.tsx +57 -0
  111. package/hover-highlighter/bubble-to-component.tsx +82 -0
  112. package/hover-highlighter/hover-highlighter.compositions.tsx +65 -0
  113. package/hover-highlighter/hover-highlighter.spec.tsx +115 -0
  114. package/hover-highlighter/hover-highlighter.tsx +8 -0
  115. package/hover-highlighter/index.ts +5 -0
  116. package/hover-highlighter/use-hover-highlighter.tsx +85 -0
  117. package/hybrid-highlighter/hybrid-highlighter.tsx +136 -0
  118. package/hybrid-highlighter/index.ts +2 -0
  119. package/ignore-highlighter.tsx +22 -0
  120. package/index.ts +21 -0
  121. package/label/component-strip.compositions.tsx +13 -0
  122. package/label/component-strip.module.scss +68 -0
  123. package/label/component-strip.tsx +57 -0
  124. package/label/index.ts +5 -0
  125. package/label/label-container.tsx +72 -0
  126. package/label/label.module.scss +32 -0
  127. package/label/label.tsx +37 -0
  128. package/label/links.tsx +9 -0
  129. package/label/other-components.tsx +51 -0
  130. package/mock-component.tsx +23 -0
  131. package/package.json +61 -0
  132. package/rule-matcher.tsx +42 -0
  133. package/types/asset.d.ts +29 -0
  134. package/types/style.d.ts +42 -0
@@ -0,0 +1,10 @@
1
+ import * as compositions_0 from '/tmp/capsules-root/sign-capsules/2025-6-8/gha9du/teambit.react_ui_component-highlighter@4d64e75088ad738b699f1df7db0bcd7201d589d0/dist/children-highlighter/children-highlighter.composition.js';
2
+ import * as compositions_1 from '/tmp/capsules-root/sign-capsules/2025-6-8/gha9du/teambit.react_ui_component-highlighter@4d64e75088ad738b699f1df7db0bcd7201d589d0/dist/element-highlighter/element-highlighter.compositions.js';
3
+ import * as compositions_2 from '/tmp/capsules-root/sign-capsules/2025-6-8/gha9du/teambit.react_ui_component-highlighter@4d64e75088ad738b699f1df7db0bcd7201d589d0/dist/hover-highlighter/hover-highlighter.compositions.js';
4
+ import * as compositions_3 from '/tmp/capsules-root/sign-capsules/2025-6-8/gha9du/teambit.react_ui_component-highlighter@4d64e75088ad738b699f1df7db0bcd7201d589d0/dist/label/component-strip.compositions.js';
5
+ import * as overview_0 from '/tmp/capsules-root/sign-capsules/2025-6-8/gha9du/teambit.react_ui_component-highlighter@4d64e75088ad738b699f1df7db0bcd7201d589d0/dist/component-highlighter.docs.md';
6
+
7
+ export const compositions = [compositions_0, compositions_1, compositions_2, compositions_3];
8
+ export const overview = [overview_0];
9
+
10
+ export const compositions_metadata = {"compositions":[{"displayName":"Children highlighter preview","identifier":"ChildrenHighlighterPreview"},{"displayName":"Children highlighter with custom colors","identifier":"ChildrenHighlighterWithCustomColors"},{"displayName":"Children highlighter inside ignore","identifier":"ChildrenHighlighterInsideIgnore"},{"displayName":"Children highlighter with rule","identifier":"ChildrenHighlighterWithRule"},{"displayName":"Children highlighter with component rule","identifier":"ChildrenHighlighterWithComponentRule"},{"displayName":"Highlighted element","identifier":"HighlightedElement"},{"displayName":"Customized","identifier":"Customized"},{"displayName":"Sizes","identifier":"Sizes"},{"displayName":"Moving element","identifier":"MovingElement"},{"displayName":"Fullscreen element","identifier":"FullscreenElement"},{"displayName":"Offscreen elements","identifier":"OffscreenElements"},{"displayName":"Show when hovering","identifier":"ShowWhenHovering"},{"displayName":"Unmounting element","identifier":"UnmountingElement"},{"displayName":"Hover exclusion zones","identifier":"HoverExclusionZones"},{"displayName":"Component strip preview","identifier":"ComponentStripPreview"}]};
@@ -0,0 +1,8 @@
1
+ import { ComponentMeta } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
2
+ export type MatchRule = undefined | string | ((element: HTMLElement) => boolean);
3
+ export type ComponentMatchRule = undefined | string | string[] | ((target: ComponentMatchTarget) => boolean);
4
+ export declare function ruleMatcher(element: HTMLElement, rule: MatchRule): boolean;
5
+ export type ComponentMatchTarget = {
6
+ meta: ComponentMeta;
7
+ };
8
+ export declare function componentRuleMatcher(target: ComponentMatchTarget, rule: ComponentMatchRule): boolean;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ruleMatcher = ruleMatcher;
4
+ exports.componentRuleMatcher = componentRuleMatcher;
5
+ // in the future, we will add more options here, like include / exclude objects.
6
+ const component_id_1 = require("@teambit/component-id");
7
+ function ruleMatcher(element, rule) {
8
+ if (typeof rule === 'string') {
9
+ return element.matches(rule);
10
+ }
11
+ if (typeof rule === 'function') {
12
+ return rule(element);
13
+ }
14
+ return true;
15
+ }
16
+ function componentRuleMatcher(target, rule) {
17
+ if (typeof rule === 'string') {
18
+ const targetCmpId = component_id_1.ComponentID.tryFromString(target.meta.id);
19
+ const ruleCmpId = component_id_1.ComponentID.tryFromString(rule);
20
+ return component_id_1.ComponentID.isEqual(ruleCmpId, targetCmpId, { ignoreVersion: true });
21
+ }
22
+ if (Array.isArray(rule)) {
23
+ const targetCmpId = component_id_1.ComponentID.tryFromString(target.meta.id);
24
+ const ruleCmpIds = rule.map((x) => component_id_1.ComponentID.tryFromString(x));
25
+ return ruleCmpIds.some((cmdId) => component_id_1.ComponentID.isEqual(targetCmpId, cmdId, { ignoreVersion: true }));
26
+ }
27
+ if (typeof rule === 'function') {
28
+ return rule(target);
29
+ }
30
+ return true;
31
+ }
32
+ //# sourceMappingURL=rule-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-matcher.js","sourceRoot":"","sources":["../rule-matcher.tsx"],"names":[],"mappings":";;AAOA,kCAUC;AAID,oDAoBC;AAzCD,gFAAgF;AAChF,wDAAoD;AAMpD,SAAgB,WAAW,CAAC,OAAoB,EAAE,IAAe;IAC/D,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAID,SAAgB,oBAAoB,CAAC,MAA4B,EAAE,IAAwB;IACzF,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,0BAAW,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9D,MAAM,SAAS,GAAG,0BAAW,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAElD,OAAO,0BAAW,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,MAAM,WAAW,GAAG,0BAAW,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC9D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,0BAAW,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,0BAAW,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACtG,CAAC;IAED,IAAI,OAAO,IAAI,KAAK,UAAU,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,130 @@
1
+ import React, { useState, createRef, useEffect, CSSProperties } from 'react';
2
+ import { ElementHighlighter } from './element-highlighter';
3
+ import { MockTarget } from '../mock-component';
4
+
5
+ type HighlightedElementProps = {
6
+ style?: CSSProperties;
7
+ targetStyle?: CSSProperties;
8
+ className?: string;
9
+ watchMotion?: boolean;
10
+ };
11
+
12
+ export const HighlightedElement = ({ style, targetStyle, watchMotion, className }: HighlightedElementProps) => {
13
+ const targetRef = createRef<HTMLDivElement>();
14
+
15
+ return (
16
+ <div className={className} style={{ padding: '16px 16px 40px 16px', width: 300, fontFamily: 'sans-serif' }}>
17
+ <div ref={targetRef} style={{ width: 100, ...targetStyle }}>
18
+ highlight target
19
+ </div>
20
+
21
+ <ElementHighlighter
22
+ targetRef={targetRef}
23
+ components={[MockTarget]}
24
+ style={style}
25
+ watchMotion={watchMotion}
26
+ placement="bottom"
27
+ />
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export const Customized = () => {
33
+ return (
34
+ <HighlightedElement
35
+ style={
36
+ {
37
+ '--bit-highlighter-color': '#94deb4',
38
+ '--bit-highlighter-color-hover': '#d0f1de',
39
+ '--bit-highlighter-color-active': '#37b26c',
40
+ } as CSSProperties
41
+ }
42
+ />
43
+ );
44
+ };
45
+
46
+ export const Sizes = () => {
47
+ return (
48
+ <div>
49
+ <HighlightedElement style={{ fontSize: 10 }} />
50
+ <HighlightedElement style={{ fontSize: 14 }} />
51
+ <HighlightedElement style={{ fontSize: 18 }} />
52
+ </div>
53
+ );
54
+ };
55
+
56
+ const fps = 30;
57
+ const frameInterval = 1000 / fps;
58
+
59
+ export const MovingElement = () => {
60
+ const [margin, setMargin] = useState(0);
61
+
62
+ useEffect(() => {
63
+ const intervalId = setInterval(() => setMargin((x) => (x + 1) % 100), frameInterval);
64
+ return () => clearInterval(intervalId);
65
+ }, []);
66
+
67
+ return <HighlightedElement targetStyle={{ marginLeft: margin }} watchMotion />;
68
+ };
69
+
70
+ export const FullscreenElement = () => {
71
+ const targetRef = createRef<HTMLDivElement>();
72
+
73
+ return (
74
+ <div style={{ fontFamily: 'sans-serif' }}>
75
+ <div
76
+ ref={targetRef}
77
+ style={{
78
+ height: '100vh',
79
+ width: '100%',
80
+ background: '#bceed4',
81
+ }}
82
+ >
83
+ This element will cover the entire document,
84
+ <br />
85
+ pushing the highlighter to the edge of the window.
86
+ <br />
87
+ The highlighter should remain inside and expand no further than the document.
88
+ </div>
89
+ <ElementHighlighter targetRef={targetRef} components={[MockTarget]} watchMotion />
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const edgeStyles = { position: 'absolute', background: 'cyan', padding: 30 } as const;
95
+ const centerStyles = {
96
+ top: { top: -30, left: '50%', transform: 'translate(-50%,0)' },
97
+ right: { right: -30, top: '50%', transform: 'translate(0, -50%)' },
98
+ bottom: { bottom: -30, left: '50%', transform: 'translate(-50%,0)' },
99
+ left: { left: -30, top: '50%', transform: 'translate(0, -50%)' },
100
+ };
101
+
102
+ export function OffscreenElements() {
103
+ const target01 = createRef<HTMLDivElement>();
104
+ const target02 = createRef<HTMLDivElement>();
105
+ const target03 = createRef<HTMLDivElement>();
106
+ const target04 = createRef<HTMLDivElement>();
107
+
108
+ return (
109
+ <div style={{ fontFamily: 'sans-serif', height: '100%' }}>
110
+ <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
111
+ <div ref={target01} style={{ ...edgeStyles, ...centerStyles.top }}>
112
+ top
113
+ </div>
114
+ <div ref={target02} style={{ ...edgeStyles, ...centerStyles.right }}>
115
+ right
116
+ </div>
117
+ <div ref={target03} style={{ ...edgeStyles, ...centerStyles.bottom }}>
118
+ bottom
119
+ </div>
120
+ <div ref={target04} style={{ ...edgeStyles, ...centerStyles.left }}>
121
+ left
122
+ </div>
123
+ <ElementHighlighter targetRef={target01} components={[MockTarget]} watchMotion />
124
+ <ElementHighlighter targetRef={target02} components={[MockTarget]} watchMotion />
125
+ <ElementHighlighter targetRef={target03} components={[MockTarget]} watchMotion />
126
+ <ElementHighlighter targetRef={target04} components={[MockTarget]} watchMotion />
127
+ </div>
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,10 @@
1
+ .label {
2
+ // space between the label and the target
3
+ // keep this space so users can move their cursor in this space.
4
+ padding: 8px;
5
+ }
6
+
7
+ .frame,
8
+ .label {
9
+ z-index: 15500 // $highlighter-z-index;
10
+ }
@@ -0,0 +1,51 @@
1
+ import React, { RefObject } from 'react';
2
+ import classnames from 'classnames';
3
+ import { ComponentMetaHolder } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
4
+ import { Frame } from '../frame';
5
+ import { Label, LabelContainer, Placement } from '../label';
6
+ import { skipHighlighterAttr } from '../ignore-highlighter';
7
+ import styles from './element-highlighter.module.scss';
8
+
9
+ export interface ElementHighlighterProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ /** highlight this element */
11
+ targetRef: RefObject<HTMLElement | null>;
12
+ /** components with metadata to show in the label */
13
+ components?: (ComponentMetaHolder | string)[];
14
+
15
+ /** default location of the label */
16
+ placement?: Placement;
17
+ /** customize styles */
18
+ classes?: HighlightClasses;
19
+ /** continually update highlighter to match moving elements */
20
+ watchMotion?: boolean;
21
+ }
22
+
23
+ export { Placement };
24
+
25
+ export type HighlightClasses = {
26
+ container?: string;
27
+ frame?: string;
28
+ label?: string;
29
+ };
30
+
31
+ export function ElementHighlighter({
32
+ targetRef,
33
+ components,
34
+ placement = 'top',
35
+ watchMotion,
36
+ className,
37
+ classes,
38
+ ...props
39
+ }: ElementHighlighterProps) {
40
+ return (
41
+ <div {...props} {...skipHighlighterAttr} className={classnames(classes?.container, styles.container, className)}>
42
+ <Frame targetRef={targetRef} className={classnames(styles.frame, classes?.frame)} watchMotion={watchMotion} />
43
+
44
+ {components && (
45
+ <LabelContainer targetRef={targetRef} className={styles.label} placement={placement} watchMotion={watchMotion}>
46
+ <Label components={components} className={classes?.label} />
47
+ </LabelContainer>
48
+ )}
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,2 @@
1
+ export { ElementHighlighter } from './element-highlighter';
2
+ export type { ElementHighlighterProps, Placement, HighlightClasses } from './element-highlighter';
@@ -0,0 +1,23 @@
1
+ $border: 2;
2
+ $padding: 4;
3
+
4
+ .overlayBorder {
5
+ box-sizing: border-box;
6
+ border: $border * 1px solid var(--bit-highlighter-color, #eebcc9);
7
+
8
+ border-radius: 11px;
9
+ padding: $padding * 1px;
10
+
11
+ pointer-events: none;
12
+ user-select: none;
13
+ }
14
+
15
+ .hidden {
16
+ // the frame's size is ignored anyways,
17
+ // and so we can use 'display: none' and not 'visibility: hidden'
18
+ display: none;
19
+ }
20
+
21
+ :export {
22
+ offset: $border + $padding;
23
+ }
@@ -0,0 +1,139 @@
1
+ import React, { useEffect, useLayoutEffect, useRef, RefObject, CSSProperties } from 'react';
2
+ import classnames from 'classnames';
3
+ import { useFloating, autoUpdate, offset, size, detectOverflow, hide } from '@floating-ui/react-dom';
4
+ import type { Coords } from '@floating-ui/react-dom';
5
+
6
+ import styles from './frame.module.scss';
7
+
8
+ /** frame padding around the target */
9
+ const MARGIN_FROM_TARGET = +styles.offset || 6; // setting fallback 6, for tests
10
+ /** min. distance from the edge of the screen. */
11
+ const MARGIN_FROM_DOC_EDGE = 0;
12
+
13
+ const overflowParameters = { rootBoundary: 'document', padding: MARGIN_FROM_DOC_EDGE } as const;
14
+ const SHIFT_POSITIVE = 'shiftPositive';
15
+
16
+ const HAS_RESIZE_OBSERVER = typeof window !== 'undefined' && !!window.ResizeObserver;
17
+
18
+ export interface FrameProps extends React.HTMLAttributes<HTMLDivElement> {
19
+ /** apply the frame to this element */
20
+ targetRef: RefObject<HTMLElement | null>;
21
+ /**
22
+ * the specific flavor of the frame.
23
+ * @default "redBorderClass"
24
+ */
25
+ stylesClass?: string;
26
+ /** continually update frame position to match moving elements */
27
+ watchMotion?: boolean;
28
+ }
29
+
30
+ // position - bottom start (bottom left corner)
31
+ // x - width - horizontal (cross axis)
32
+ // y - height - vertical (main axis)
33
+
34
+ type DimensionsStyle = Pick<CSSProperties, 'width' | 'height' | 'maxWidth' | 'maxHeight'>;
35
+
36
+ export function Frame({ targetRef, watchMotion, className, stylesClass = styles.overlayBorder, style }: FrameProps) {
37
+ const dimensionRef = useRef<DimensionsStyle>({ width: 0, height: 0 });
38
+
39
+ const { x, y, strategy, reference, floating, update, refs, middlewareData } = useFloating({
40
+ placement: 'bottom-start',
41
+ middleware: [
42
+ // replace dimensions from previous iterations with the target's size
43
+ // this is only the measured size, not yet the applied size
44
+ {
45
+ name: 'align-to-target',
46
+ fn({ rects }) {
47
+ rects.floating = {
48
+ ...rects.floating,
49
+ width: rects.reference.width + 2 * MARGIN_FROM_TARGET,
50
+ height: rects.reference.height + 2 * MARGIN_FROM_TARGET,
51
+ };
52
+
53
+ return {};
54
+ },
55
+ },
56
+ // reposition x,y, to the top of the reference
57
+ offset(({ rects }) => -rects.reference.height),
58
+ // offset the frame by its extra padding
59
+ offset(() => ({ mainAxis: -MARGIN_FROM_TARGET, crossAxis: -MARGIN_FROM_TARGET })),
60
+ // pushes the frame into the document. Similar to shift(), but only pushes when coods are negative
61
+ {
62
+ name: 'shiftPositive',
63
+ fn: async (args) => {
64
+ const overflow = await detectOverflow(args, overflowParameters);
65
+
66
+ const nextCoords = {
67
+ x: overflow.left > 0 ? args.x + overflow.left : args.x,
68
+ y: overflow.top > 0 ? args.y + overflow.top : args.y,
69
+ };
70
+ const shiftAmount = {
71
+ x: nextCoords.x - args.x,
72
+ y: nextCoords.y - args.y,
73
+ };
74
+
75
+ return {
76
+ ...nextCoords,
77
+ data: shiftAmount,
78
+ };
79
+ },
80
+ },
81
+ // size also applies overflow detection via width and height
82
+ size({
83
+ // apply overflow detection in reference to the document
84
+ rootBoundary: 'document',
85
+ padding: MARGIN_FROM_DOC_EDGE,
86
+ apply({ elements, rects, availableHeight, availableWidth, middlewareData }) {
87
+ const shift: Coords = middlewareData[SHIFT_POSITIVE] || { x: 0, y: 0 };
88
+ const paddingX = 2 * MARGIN_FROM_TARGET - shift.x;
89
+ const paddingY = 2 * MARGIN_FROM_TARGET - shift.y;
90
+
91
+ const dimensions: DimensionsStyle = {
92
+ width: rects.reference.width + paddingX,
93
+ height: rects.reference.height + paddingY,
94
+ maxWidth: availableWidth,
95
+ maxHeight: availableHeight,
96
+ };
97
+
98
+ // per floating-ui docs, apply styles directly during apply()
99
+ Object.assign(elements.floating.style, dimensions);
100
+
101
+ // also store in reference, so react renders will have the same value
102
+ dimensionRef.current = dimensions;
103
+ },
104
+ }),
105
+ hide({ strategy: 'referenceHidden' }),
106
+ ],
107
+ });
108
+
109
+ // set target as floating reference
110
+ useLayoutEffect(() => {
111
+ reference(targetRef.current);
112
+ }, [targetRef.current]);
113
+
114
+ // automatically update on scroll, resize, etc.
115
+ // `watchMotion` will trigger continuous updates using animation frame
116
+ useEffect(() => {
117
+ if (!refs.reference.current || !refs.floating.current || !HAS_RESIZE_OBSERVER) return () => {};
118
+
119
+ return autoUpdate(refs.reference.current, refs.floating.current, update, { animationFrame: watchMotion });
120
+ }, [refs.reference.current, refs.floating.current, update, watchMotion]);
121
+
122
+ // could check if x !== null
123
+ const isReady = !middlewareData.hide?.referenceHidden;
124
+
125
+ return (
126
+ <div
127
+ ref={floating}
128
+ className={classnames(className, stylesClass, !isReady && styles.hidden)}
129
+ style={{
130
+ ...style,
131
+ ...dimensionRef.current,
132
+ position: strategy,
133
+ // starting at pos [0,0] will ensure the label doesn't increase the document size.
134
+ top: y ?? 0,
135
+ left: x ?? 0,
136
+ }}
137
+ />
138
+ );
139
+ }
package/frame/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Frame } from './frame';
2
+ export type { FrameProps } from './frame';
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { MockTarget } from '../mock-component';
4
+ import { bubbleToComponent } from './bubble-to-component';
5
+
6
+ it('should find component when starting from a div', () => {
7
+ const { getByText, getByTestId } = render(
8
+ <MockTarget data-testid="expected-result">
9
+ <div>hello world</div>
10
+ </MockTarget>
11
+ );
12
+
13
+ const rendered = getByText('hello world');
14
+
15
+ const result = bubbleToComponent(rendered);
16
+
17
+ expect(result?.element).toBe(getByTestId('expected-result'));
18
+ expect(result?.components).toEqual([MockTarget]);
19
+ });
20
+
21
+ it('should bubble to root component when it renders itself recursively', () => {
22
+ const { getByText, getByTestId } = render(
23
+ <MockTarget data-testid="expected-result">
24
+ <MockTarget>
25
+ <MockTarget>
26
+ <div>hello world</div>
27
+ </MockTarget>
28
+ </MockTarget>
29
+ </MockTarget>
30
+ );
31
+
32
+ const rendered = getByText('hello world');
33
+
34
+ const result = bubbleToComponent(rendered);
35
+
36
+ expect(result?.element).toBe(getByTestId('expected-result'));
37
+ expect(result?.components).toEqual([MockTarget]);
38
+ });
39
+
40
+ it('should find first component, when parent propagation is disabled', () => {
41
+ const { getByText, getByTestId } = render(
42
+ <MockTarget>
43
+ <MockTarget>
44
+ <MockTarget data-testid="expected-result">
45
+ <div>hello world</div>
46
+ </MockTarget>
47
+ </MockTarget>
48
+ </MockTarget>
49
+ );
50
+
51
+ const rendered = getByText('hello world');
52
+
53
+ const result = bubbleToComponent(rendered, { propagateSameParents: false });
54
+
55
+ expect(result?.element).toBe(getByTestId('expected-result'));
56
+ expect(result?.components).toEqual([MockTarget]);
57
+ });
@@ -0,0 +1,82 @@
1
+ import { domToReacts, toRootElement } from '@teambit/react.modules.dom-to-react';
2
+ import {
3
+ componentMetaField,
4
+ hasComponentMeta,
5
+ ReactComponentMetaHolder,
6
+ } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
7
+ import { ruleMatcher, MatchRule, ComponentMatchRule, componentRuleMatcher } from '../rule-matcher';
8
+
9
+ type BubblingOptions = {
10
+ /** filter elements by this rule */
11
+ elementRule?: MatchRule;
12
+ /** filter components by this rule */
13
+ componentRule?: ComponentMatchRule;
14
+ /**
15
+ * continue bubbling when encountering a parent of the same component
16
+ * @default true
17
+ */
18
+ propagateSameParents?: boolean;
19
+ };
20
+
21
+ /** go up the dom tree until reaching a react bit component */
22
+ export function bubbleToComponent(
23
+ element: HTMLElement | null,
24
+ { elementRule, componentRule, propagateSameParents = true }: BubblingOptions = {}
25
+ ) {
26
+ // check if the element is rendered in Vue
27
+ if ((element as any)?.__vnode) {
28
+ const vueComp = (element as any).__vnode?.ctx?.type;
29
+ const comp = vueComp?.__bit_component;
30
+ const compEl = (element as any).__vnode?.ctx?.ctx?.$el;
31
+ if (compEl && comp) {
32
+ return {
33
+ element: compEl,
34
+ components: [{ [componentMetaField]: comp }],
35
+ };
36
+ }
37
+ }
38
+
39
+ let current = bubbleToFirstComponent(element, elementRule, componentRule);
40
+ if (!propagateSameParents) return current;
41
+
42
+ while (current) {
43
+ const parentElement = current.element.parentElement;
44
+ const parent = bubbleToFirstComponent(parentElement, elementRule, componentRule);
45
+
46
+ const primeComponent = current?.components.slice(-1).pop();
47
+ const parentPrimeComponent = parent?.components.slice(-1).pop();
48
+
49
+ if (primeComponent?.[componentMetaField].id !== parentPrimeComponent?.[componentMetaField].id) return current;
50
+
51
+ current = parent;
52
+ }
53
+
54
+ return undefined;
55
+ }
56
+
57
+ /** go up the dom tree until reaching a react bit component */
58
+ function bubbleToFirstComponent(
59
+ element: HTMLElement | null,
60
+ elementRule?: MatchRule,
61
+ componentRule?: ComponentMatchRule
62
+ ) {
63
+ for (let current = element; current; current = current.parentElement) {
64
+ current = toRootElement(current);
65
+ if (!current) return undefined;
66
+ if (ruleMatcher(current, elementRule)) {
67
+ const components = domToReacts(current);
68
+
69
+ const relevantComponents = components.filter(
70
+ (x) => hasComponentMeta(x) && componentRuleMatcher({ meta: x[componentMetaField] }, componentRule)
71
+ ) as ReactComponentMetaHolder[];
72
+
73
+ if (relevantComponents.length < 1) return undefined;
74
+ return {
75
+ element: current,
76
+ components: relevantComponents,
77
+ };
78
+ }
79
+ }
80
+
81
+ return undefined;
82
+ }
@@ -0,0 +1,65 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { HoverHighlighter } from './hover-highlighter';
3
+ import { MockButton, MockTarget } from '../mock-component';
4
+ import { excludeHighlighterAtt } from '../ignore-highlighter';
5
+
6
+ export const ShowWhenHovering = () => {
7
+ const [disabled, setDisabled] = useState<boolean>(false);
8
+
9
+ return (
10
+ <div style={{ padding: '16px 50px 32px 16px', minWidth: 300, fontFamily: 'sans-serif' }}>
11
+ <HoverHighlighter style={{ padding: 16 }} disabled={disabled}>
12
+ <div>
13
+ <br />
14
+ <div>
15
+ <MockButton onClick={() => setDisabled((x) => !x)}>Hover here</MockButton>
16
+ </div>
17
+ <div>
18
+ {disabled ? 'X' : '✓'} highlighter is {disabled ? 'disabled' : 'enabled'}
19
+ </div>
20
+ </div>
21
+ </HoverHighlighter>
22
+ </div>
23
+ );
24
+ };
25
+
26
+ export const UnmountingElement = () => {
27
+ const [shown, setShown] = useState(true);
28
+ useEffect(() => {
29
+ const tid = setInterval(() => setShown((x) => !x), 1500);
30
+ return () => clearInterval(tid);
31
+ }, []);
32
+
33
+ return (
34
+ <div style={{ padding: '16px 50px 32px 16px', minWidth: 300, fontFamily: 'sans-serif' }}>
35
+ <HoverHighlighter>
36
+ <div>{!shown && '(hidden)'}</div>
37
+
38
+ <div>{shown && <MockButton>Hover here</MockButton>}</div>
39
+ <br />
40
+ <MockTarget>
41
+ <div>{shown && <MockButton>Hover here</MockButton>}</div>
42
+ <div>same with a container</div>
43
+ </MockTarget>
44
+ </HoverHighlighter>
45
+ </div>
46
+ );
47
+ };
48
+
49
+ export const HoverExclusionZones = () => {
50
+ return (
51
+ <div style={{ padding: '16px 50px 32px 16px', minWidth: 300, fontFamily: 'sans-serif' }}>
52
+ <HoverHighlighter>
53
+ <MockTarget>
54
+ container (target-able)
55
+ <div>{<MockButton>will be highlighted</MockButton>}</div>
56
+ </MockTarget>
57
+ <br />
58
+ <MockTarget>
59
+ container (target-able)
60
+ <div {...excludeHighlighterAtt}>{<MockButton>will be ignored</MockButton>}</div>
61
+ </MockTarget>
62
+ </HoverHighlighter>
63
+ </div>
64
+ );
65
+ };