@teambit/react.ui.component-highlighter 0.0.0-2240a868c0f88bba4f62719e6d10d31987cae1f9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/children-highlighter/children-highlighter.composition.tsx +103 -0
- package/children-highlighter/children-highlighter.spec.tsx +22 -0
- package/children-highlighter/children-highlighter.tsx +9 -0
- package/children-highlighter/index.ts +5 -0
- package/children-highlighter/use-children-highlighter.tsx +79 -0
- package/component-highlighter.docs.md +191 -0
- package/dist/children-highlighter/children-highlighter.composition.d.ts +6 -0
- package/dist/children-highlighter/children-highlighter.composition.js +93 -0
- package/dist/children-highlighter/children-highlighter.composition.js.map +1 -0
- package/dist/children-highlighter/children-highlighter.d.ts +4 -0
- package/dist/children-highlighter/children-highlighter.js +24 -0
- package/dist/children-highlighter/children-highlighter.js.map +1 -0
- package/dist/children-highlighter/children-highlighter.spec.d.ts +1 -0
- package/dist/children-highlighter/children-highlighter.spec.js +22 -0
- package/dist/children-highlighter/children-highlighter.spec.js.map +1 -0
- package/dist/children-highlighter/index.d.ts +4 -0
- package/dist/children-highlighter/index.js +8 -0
- package/dist/children-highlighter/index.js.map +1 -0
- package/dist/children-highlighter/use-children-highlighter.d.ts +18 -0
- package/dist/children-highlighter/use-children-highlighter.js +51 -0
- package/dist/children-highlighter/use-children-highlighter.js.map +1 -0
- package/dist/component-highlighter.docs.md +191 -0
- package/dist/element-highlighter/element-highlighter.compositions.d.ts +14 -0
- package/dist/element-highlighter/element-highlighter.compositions.js +113 -0
- package/dist/element-highlighter/element-highlighter.compositions.js.map +1 -0
- package/dist/element-highlighter/element-highlighter.d.ts +22 -0
- package/dist/element-highlighter/element-highlighter.js +31 -0
- package/dist/element-highlighter/element-highlighter.js.map +1 -0
- package/dist/element-highlighter/element-highlighter.module.scss +10 -0
- package/dist/element-highlighter/index.d.ts +2 -0
- package/dist/element-highlighter/index.js +6 -0
- package/dist/element-highlighter/index.js.map +1 -0
- package/dist/frame/frame.d.ts +14 -0
- package/dist/frame/frame.js +138 -0
- package/dist/frame/frame.js.map +1 -0
- package/dist/frame/frame.module.scss +23 -0
- package/dist/frame/index.d.ts +2 -0
- package/dist/frame/index.js +6 -0
- package/dist/frame/index.js.map +1 -0
- package/dist/hover-highlighter/bubble-to-component.d.ts +24 -0
- package/dist/hover-highlighter/bubble-to-component.js +55 -0
- package/dist/hover-highlighter/bubble-to-component.js.map +1 -0
- package/dist/hover-highlighter/bubble-to-component.spec.d.ts +1 -0
- package/dist/hover-highlighter/bubble-to-component.spec.js +38 -0
- package/dist/hover-highlighter/bubble-to-component.spec.js.map +1 -0
- package/dist/hover-highlighter/hover-highlighter.compositions.d.ts +4 -0
- package/dist/hover-highlighter/hover-highlighter.compositions.js +83 -0
- package/dist/hover-highlighter/hover-highlighter.compositions.js.map +1 -0
- package/dist/hover-highlighter/hover-highlighter.d.ts +4 -0
- package/dist/hover-highlighter/hover-highlighter.js +24 -0
- package/dist/hover-highlighter/hover-highlighter.js.map +1 -0
- package/dist/hover-highlighter/hover-highlighter.spec.d.ts +1 -0
- package/dist/hover-highlighter/hover-highlighter.spec.js +95 -0
- package/dist/hover-highlighter/hover-highlighter.spec.js.map +1 -0
- package/dist/hover-highlighter/index.d.ts +4 -0
- package/dist/hover-highlighter/index.js +8 -0
- package/dist/hover-highlighter/index.js.map +1 -0
- package/dist/hover-highlighter/use-hover-highlighter.d.ts +25 -0
- package/dist/hover-highlighter/use-hover-highlighter.js +47 -0
- package/dist/hover-highlighter/use-hover-highlighter.js.map +1 -0
- package/dist/hybrid-highlighter/hybrid-highlighter.d.ts +36 -0
- package/dist/hybrid-highlighter/hybrid-highlighter.js +93 -0
- package/dist/hybrid-highlighter/hybrid-highlighter.js.map +1 -0
- package/dist/hybrid-highlighter/index.d.ts +2 -0
- package/dist/hybrid-highlighter/index.js +6 -0
- package/dist/hybrid-highlighter/index.js.map +1 -0
- package/dist/ignore-highlighter.d.ts +19 -0
- package/dist/ignore-highlighter.js +25 -0
- package/dist/ignore-highlighter.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/label/component-strip.compositions.d.ts +2 -0
- package/dist/label/component-strip.compositions.js +17 -0
- package/dist/label/component-strip.compositions.js.map +1 -0
- package/dist/label/component-strip.d.ts +7 -0
- package/dist/label/component-strip.js +71 -0
- package/dist/label/component-strip.js.map +1 -0
- package/dist/label/component-strip.module.scss +68 -0
- package/dist/label/index.d.ts +4 -0
- package/dist/label/index.js +8 -0
- package/dist/label/index.js.map +1 -0
- package/dist/label/label-container.d.ts +13 -0
- package/dist/label/label-container.js +87 -0
- package/dist/label/label-container.js.map +1 -0
- package/dist/label/label.d.ts +6 -0
- package/dist/label/label.js +70 -0
- package/dist/label/label.js.map +1 -0
- package/dist/label/label.module.scss +32 -0
- package/dist/label/links.d.ts +2 -0
- package/dist/label/links.js +16 -0
- package/dist/label/links.js.map +1 -0
- package/dist/label/other-components.d.ts +9 -0
- package/dist/label/other-components.js +34 -0
- package/dist/label/other-components.js.map +1 -0
- package/dist/mock-component.d.ts +14 -0
- package/dist/mock-component.js +43 -0
- package/dist/mock-component.js.map +1 -0
- package/dist/preview-1768840768294.js +10 -0
- package/dist/rule-matcher.d.ts +8 -0
- package/dist/rule-matcher.js +32 -0
- package/dist/rule-matcher.js.map +1 -0
- package/element-highlighter/element-highlighter.compositions.tsx +130 -0
- package/element-highlighter/element-highlighter.module.scss +10 -0
- package/element-highlighter/element-highlighter.tsx +51 -0
- package/element-highlighter/index.ts +2 -0
- package/frame/frame.module.scss +23 -0
- package/frame/frame.tsx +142 -0
- package/frame/index.ts +2 -0
- package/hover-highlighter/bubble-to-component.spec.tsx +57 -0
- package/hover-highlighter/bubble-to-component.tsx +82 -0
- package/hover-highlighter/hover-highlighter.compositions.tsx +65 -0
- package/hover-highlighter/hover-highlighter.spec.tsx +115 -0
- package/hover-highlighter/hover-highlighter.tsx +8 -0
- package/hover-highlighter/index.ts +5 -0
- package/hover-highlighter/use-hover-highlighter.tsx +85 -0
- package/hybrid-highlighter/hybrid-highlighter.tsx +142 -0
- package/hybrid-highlighter/index.ts +2 -0
- package/ignore-highlighter.tsx +22 -0
- package/index.ts +21 -0
- package/label/component-strip.compositions.tsx +13 -0
- package/label/component-strip.module.scss +68 -0
- package/label/component-strip.tsx +57 -0
- package/label/index.ts +5 -0
- package/label/label-container.tsx +74 -0
- package/label/label.module.scss +32 -0
- package/label/label.tsx +37 -0
- package/label/links.tsx +9 -0
- package/label/other-components.tsx +51 -0
- package/mock-component.tsx +23 -0
- package/package.json +60 -0
- package/rule-matcher.tsx +42 -0
- package/types/asset.d.ts +29 -0
- package/types/style.d.ts +42 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { act } from 'react-dom/test-utils';
|
|
3
|
+
import { render, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
|
|
4
|
+
import { ComponentMeta, componentMetaField } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
|
|
5
|
+
|
|
6
|
+
import { HoverHighlighter } from './hover-highlighter';
|
|
7
|
+
|
|
8
|
+
const debounceTime = 2;
|
|
9
|
+
|
|
10
|
+
function ButtonComponent({ children }: { children: ReactNode }) {
|
|
11
|
+
return <button>{children}</button>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
ButtonComponent[componentMetaField] = {
|
|
15
|
+
// could use a non-bit-id to render the "default" bubble
|
|
16
|
+
id: 'teambit.base-ui/input/button',
|
|
17
|
+
} as ComponentMeta;
|
|
18
|
+
|
|
19
|
+
it('should show bubble when hovering on element with bit id', async () => {
|
|
20
|
+
const { getByText, findByText } = render(
|
|
21
|
+
<HoverHighlighter debounceSelection={debounceTime}>
|
|
22
|
+
<ButtonComponent>hover here</ButtonComponent>
|
|
23
|
+
</HoverHighlighter>
|
|
24
|
+
);
|
|
25
|
+
const rendered = getByText('hover here');
|
|
26
|
+
expect(rendered).toBeTruthy();
|
|
27
|
+
|
|
28
|
+
act(() => {
|
|
29
|
+
fireEvent.mouseOver(rendered);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const highlightBubble = await findByText('input/button');
|
|
33
|
+
expect(highlightBubble).toBeInstanceOf(HTMLElement);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should hide the highlight when hovering out of the element', async () => {
|
|
37
|
+
const { getByText, findByText, queryByText } = render(
|
|
38
|
+
<HoverHighlighter debounceSelection={debounceTime}>
|
|
39
|
+
<ButtonComponent>hover here</ButtonComponent>
|
|
40
|
+
</HoverHighlighter>
|
|
41
|
+
);
|
|
42
|
+
const rendered = getByText('hover here');
|
|
43
|
+
|
|
44
|
+
act(() => {
|
|
45
|
+
fireEvent.mouseOver(rendered);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const highlightBubble = await findByText('input/button');
|
|
49
|
+
expect(highlightBubble).toBeInstanceOf(HTMLElement);
|
|
50
|
+
|
|
51
|
+
act(() => {
|
|
52
|
+
fireEvent.mouseOut(rendered);
|
|
53
|
+
});
|
|
54
|
+
await waitForElementToBeRemoved(() => queryByText('input/button'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should keep the highlighter, when hovering on it (even when moving out of the component zone)', async () => {
|
|
58
|
+
const { getByText, findByText } = render(
|
|
59
|
+
<HoverHighlighter debounceSelection={debounceTime}>
|
|
60
|
+
<ButtonComponent>hover here</ButtonComponent>
|
|
61
|
+
</HoverHighlighter>
|
|
62
|
+
);
|
|
63
|
+
const rendered = getByText('hover here');
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
// hover on target element:
|
|
67
|
+
fireEvent.mouseOver(rendered);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const highlightBubble = await findByText('input/button');
|
|
71
|
+
|
|
72
|
+
await act(async () => {
|
|
73
|
+
// move mouse out of target element, "towards" the highlighter bubble
|
|
74
|
+
// this should trigger hiding
|
|
75
|
+
fireEvent.mouseOut(rendered);
|
|
76
|
+
|
|
77
|
+
// move mouse into the highlighter bubble
|
|
78
|
+
fireEvent.mouseEnter(highlightBubble);
|
|
79
|
+
// allow react to update state during the act()
|
|
80
|
+
// and before verifying highlighter remains
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, debounceTime + 10));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// highlighter should still focus the target button
|
|
85
|
+
expect(await findByText('input/button')).toBeInstanceOf(HTMLElement);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should hide the highlighter when moving the mouse away of it', async () => {
|
|
89
|
+
const { getByText, queryByText, findByText } = render(
|
|
90
|
+
<HoverHighlighter debounceSelection={debounceTime}>
|
|
91
|
+
<ButtonComponent>hover here</ButtonComponent>
|
|
92
|
+
</HoverHighlighter>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const rendered = getByText('hover here');
|
|
96
|
+
|
|
97
|
+
// hover on target element:
|
|
98
|
+
await act(async () => {
|
|
99
|
+
fireEvent.mouseOver(rendered);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const highlightBubble = await findByText('input/button');
|
|
103
|
+
|
|
104
|
+
await act(async () => {
|
|
105
|
+
// hover on highlighter
|
|
106
|
+
fireEvent.mouseEnter(highlightBubble);
|
|
107
|
+
// leave the highlighter
|
|
108
|
+
fireEvent.mouseOut(highlightBubble);
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, debounceTime + 10));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// highlighter sometimes disappears before this check,
|
|
113
|
+
// so not using waitForElementToBeRemoved, and using setTimeout instead
|
|
114
|
+
expect(queryByText('input/button')).toBeNull();
|
|
115
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { HybridHighlighter, HybridHighlighterProps } from '../hybrid-highlighter';
|
|
3
|
+
|
|
4
|
+
export type HoverHighlighterProps = Omit<HybridHighlighterProps, 'mode'>;
|
|
5
|
+
|
|
6
|
+
export function HoverHighlighter({ ...props }: HoverHighlighterProps) {
|
|
7
|
+
return <HybridHighlighter {...props} mode={'hover'} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useDebouncedCallback } from 'use-debounce';
|
|
3
|
+
import { useHoverSelection } from '@teambit/react.ui.hover-selector';
|
|
4
|
+
import { ComponentMetaHolder } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
|
|
5
|
+
|
|
6
|
+
import { excludeHighlighterSelector, skipHighlighterSelector } from '../ignore-highlighter';
|
|
7
|
+
import { MatchRule, ComponentMatchRule } from '../rule-matcher';
|
|
8
|
+
import { bubbleToComponent } from './bubble-to-component';
|
|
9
|
+
|
|
10
|
+
type HighlightTarget = { element: HTMLElement; components: ComponentMetaHolder[] };
|
|
11
|
+
export type useHoverHighlighterOptions = {
|
|
12
|
+
debounceDuration: number;
|
|
13
|
+
scopeClass: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** filter highlighter targets by this query selector. (May be a more complex object in the future) */
|
|
16
|
+
rule?: MatchRule;
|
|
17
|
+
/** filter targets by this component match rule */
|
|
18
|
+
componentRule?: ComponentMatchRule;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** fires onChange when targeting a new component */
|
|
22
|
+
export function useHoverHighlighter<T extends HTMLElement = HTMLElement>(
|
|
23
|
+
onChange: (target?: HighlightTarget) => void,
|
|
24
|
+
props: React.HTMLAttributes<T> = {},
|
|
25
|
+
{ debounceDuration, scopeClass, disabled, rule, componentRule }: useHoverHighlighterOptions
|
|
26
|
+
) {
|
|
27
|
+
const { handleElement } = useHoverHandler({ onChange, scopeClass, debounceDuration, disabled, rule, componentRule });
|
|
28
|
+
|
|
29
|
+
const handlers = useHoverSelection(disabled ? undefined : handleElement, props);
|
|
30
|
+
|
|
31
|
+
return handlers;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type useHoverHighlighterProps = {
|
|
35
|
+
onChange: (target?: HighlightTarget) => void;
|
|
36
|
+
scopeClass?: string;
|
|
37
|
+
debounceDuration?: number;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
rule?: MatchRule;
|
|
40
|
+
componentRule?: ComponentMatchRule;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function useHoverHandler({
|
|
44
|
+
onChange,
|
|
45
|
+
scopeClass = '',
|
|
46
|
+
debounceDuration,
|
|
47
|
+
disabled,
|
|
48
|
+
rule,
|
|
49
|
+
componentRule,
|
|
50
|
+
}: useHoverHighlighterProps) {
|
|
51
|
+
// debounced method is ref'ed, so no need for useCallback
|
|
52
|
+
const _handleElement = (element: HTMLElement | null) => {
|
|
53
|
+
// clear highlighter at the edges:
|
|
54
|
+
if (!element || element.hasAttribute('data-nullify-component-highlight')) {
|
|
55
|
+
onChange(undefined);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// clear when ancestor has 'data-ignore-component-highlight'
|
|
60
|
+
if (element.closest(`.${scopeClass} ${excludeHighlighterSelector}`)) {
|
|
61
|
+
onChange(undefined);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// skip DOM trees having 'data-skip-component-highlight'
|
|
66
|
+
if (element.closest(`.${scopeClass} ${skipHighlighterSelector}`)) return;
|
|
67
|
+
|
|
68
|
+
const result = bubbleToComponent(element, { elementRule: rule, componentRule });
|
|
69
|
+
if (!result) return;
|
|
70
|
+
|
|
71
|
+
onChange({
|
|
72
|
+
element: result.element,
|
|
73
|
+
components: result.components,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleElement = useDebouncedCallback(_handleElement, debounceDuration);
|
|
78
|
+
|
|
79
|
+
// clear when disabling
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (disabled) handleElement.cancel();
|
|
82
|
+
}, [disabled, handleElement]);
|
|
83
|
+
|
|
84
|
+
return { handleElement };
|
|
85
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo, useRef, createRef, CSSProperties } from 'react';
|
|
2
|
+
import classnames from 'classnames';
|
|
3
|
+
import { v4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
import { ComponentMetaHolder } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
|
|
6
|
+
|
|
7
|
+
import { useHoverHighlighter } from '../hover-highlighter';
|
|
8
|
+
import { ElementHighlighter, Placement, HighlightClasses } from '../element-highlighter';
|
|
9
|
+
import { useChildrenHighlighter } from '../children-highlighter';
|
|
10
|
+
import type { MatchRule, ComponentMatchRule } from '../rule-matcher';
|
|
11
|
+
|
|
12
|
+
type HighlightTarget = { element: HTMLElement; components: ComponentMetaHolder[] };
|
|
13
|
+
|
|
14
|
+
export interface HybridHighlighterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
15
|
+
/** stop all highlighting and drop listeners */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** default pop location for the label */
|
|
18
|
+
placement?: Placement;
|
|
19
|
+
/** customize styles */
|
|
20
|
+
classes?: HighlightClasses;
|
|
21
|
+
/** customize highlighter */
|
|
22
|
+
highlightStyle?: CSSProperties;
|
|
23
|
+
/** debounces element hover selection.
|
|
24
|
+
* A higher value will reduce element lookups as well as "keep" the highlight on the current element for longer.
|
|
25
|
+
* Initial selection (when no element is currently selected) will always happen immediately to improve the user experience.
|
|
26
|
+
* @default 80ms
|
|
27
|
+
*/
|
|
28
|
+
debounceSelection?: number;
|
|
29
|
+
/** continually update frame position to match moving elements */
|
|
30
|
+
watchMotion?: boolean;
|
|
31
|
+
|
|
32
|
+
/** filter highlighter targets by this query selector. (May be a more complex object in the future) */
|
|
33
|
+
rule?: MatchRule;
|
|
34
|
+
/** filter components to match this rule. Can be id, array of ids, or a function */
|
|
35
|
+
componentRule?: ComponentMatchRule;
|
|
36
|
+
|
|
37
|
+
/** set the behavior of the highlighter.
|
|
38
|
+
* `disabled` - stops highlighting.
|
|
39
|
+
* `allChildren` - highlights all components rendered under children
|
|
40
|
+
* `hover` - highlighters the component immediately under the mouse cursor
|
|
41
|
+
* */
|
|
42
|
+
mode?: 'allChildren' | 'hover';
|
|
43
|
+
bgColor?: string;
|
|
44
|
+
bgColorHover?: string;
|
|
45
|
+
bgColorActive?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** automatically highlight components on hover */
|
|
49
|
+
export function HybridHighlighter({
|
|
50
|
+
disabled,
|
|
51
|
+
mode = 'hover',
|
|
52
|
+
debounceSelection = 80,
|
|
53
|
+
watchMotion = true,
|
|
54
|
+
placement,
|
|
55
|
+
rule,
|
|
56
|
+
componentRule,
|
|
57
|
+
|
|
58
|
+
classes,
|
|
59
|
+
highlightStyle,
|
|
60
|
+
className,
|
|
61
|
+
style,
|
|
62
|
+
bgColor,
|
|
63
|
+
bgColorHover,
|
|
64
|
+
bgColorActive,
|
|
65
|
+
children,
|
|
66
|
+
...rest
|
|
67
|
+
}: HybridHighlighterProps) {
|
|
68
|
+
const ref = createRef<HTMLDivElement>();
|
|
69
|
+
const [targets, setTarget] = useState<Record<string, HighlightTarget>>({});
|
|
70
|
+
// const scopeClass = useRef(`hl-scope-${v4()}`).current;
|
|
71
|
+
const [scopeClass, setScopeClass] = useState<string>('');
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setScopeClass(`hl-scope-${v4()}`);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const hasTargets = Object.entries(targets).length > 0;
|
|
78
|
+
|
|
79
|
+
// clear targets when disabled
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (disabled) setTarget({});
|
|
82
|
+
}, [disabled]);
|
|
83
|
+
|
|
84
|
+
const handlers = useHoverHighlighter(
|
|
85
|
+
(nextTarget) => setTarget(nextTarget ? { 'hover-target': nextTarget } : {}),
|
|
86
|
+
rest,
|
|
87
|
+
{
|
|
88
|
+
debounceDuration: hasTargets ? debounceSelection : 0,
|
|
89
|
+
scopeClass,
|
|
90
|
+
disabled: disabled || mode !== 'hover',
|
|
91
|
+
rule,
|
|
92
|
+
componentRule,
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
useChildrenHighlighter({
|
|
97
|
+
onChange: setTarget,
|
|
98
|
+
scopeRef: ref,
|
|
99
|
+
scopeClass,
|
|
100
|
+
disabled: disabled || mode !== 'allChildren',
|
|
101
|
+
rule,
|
|
102
|
+
componentRule,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const _styles = useMemo(
|
|
106
|
+
() => ({
|
|
107
|
+
'--bit-highlighter-color': bgColor,
|
|
108
|
+
'--bit-highlighter-color-hover': bgColorHover,
|
|
109
|
+
'--bit-highlighter-color-active': bgColorActive,
|
|
110
|
+
...style,
|
|
111
|
+
}),
|
|
112
|
+
[bgColor, bgColorHover, bgColorActive, style]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
ref={ref}
|
|
118
|
+
{...rest}
|
|
119
|
+
{...handlers}
|
|
120
|
+
style={_styles}
|
|
121
|
+
className={classnames(className, scopeClass)}
|
|
122
|
+
data-nullify-component-highlight
|
|
123
|
+
>
|
|
124
|
+
{children}
|
|
125
|
+
{/*
|
|
126
|
+
* keep the highlighter inside of the hover selector, or it could disappear when switching between elements
|
|
127
|
+
* the excludeHighlighterAtt will ensure it doesn't turn into a recursion.
|
|
128
|
+
*/}
|
|
129
|
+
{Object.entries(targets).map(([key, target]) => (
|
|
130
|
+
<ElementHighlighter
|
|
131
|
+
key={key}
|
|
132
|
+
targetRef={{ current: target.element }}
|
|
133
|
+
components={target.components}
|
|
134
|
+
classes={classes}
|
|
135
|
+
style={highlightStyle}
|
|
136
|
+
placement={placement}
|
|
137
|
+
watchMotion={watchMotion}
|
|
138
|
+
/>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/** name of ignore attribute */
|
|
4
|
+
export const excludeHighlighterAttrName = 'data-ignore-component-highlight';
|
|
5
|
+
|
|
6
|
+
/** selector for elements with the ignore attribute */
|
|
7
|
+
export const excludeHighlighterSelector = `[${excludeHighlighterAttrName}]`;
|
|
8
|
+
|
|
9
|
+
/** highlighter will exclude elements with this attribute */
|
|
10
|
+
export const excludeHighlighterAtt = { [excludeHighlighterAttrName]: true };
|
|
11
|
+
|
|
12
|
+
/** children of this element will be excluded by the automatic highlighter */
|
|
13
|
+
export function ExcludeHighlighter(props: React.HTMLAttributes<HTMLDivElement>) {
|
|
14
|
+
return <div {...props} {...excludeHighlighterAtt} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** name of skip attribute */
|
|
18
|
+
export const skipHighlighterAttrName = 'data-skip-component-highlight';
|
|
19
|
+
/** highlighter will skip (ignore) elements with these attributes */
|
|
20
|
+
export const skipHighlighterAttr = { [skipHighlighterAttrName]: true };
|
|
21
|
+
/** selector for elements with the skip attribute */
|
|
22
|
+
export const skipHighlighterSelector = `[${skipHighlighterAttrName}]`;
|
package/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { HybridHighlighter as ComponentHighlighter } from './hybrid-highlighter';
|
|
2
|
+
export type { HybridHighlighterProps as ComponentHighlightProps } from './hybrid-highlighter';
|
|
3
|
+
|
|
4
|
+
export { HoverHighlighter } from './hover-highlighter';
|
|
5
|
+
export type { HoverHighlighterProps } from './hover-highlighter';
|
|
6
|
+
|
|
7
|
+
export { ChildrenHighlighter } from './children-highlighter';
|
|
8
|
+
export type { ChildrenHighlighterProps } from './children-highlighter';
|
|
9
|
+
|
|
10
|
+
export { ElementHighlighter } from './element-highlighter';
|
|
11
|
+
export type { ElementHighlighterProps, Placement, HighlightClasses } from './element-highlighter';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
ExcludeHighlighter,
|
|
15
|
+
excludeHighlighterAtt,
|
|
16
|
+
excludeHighlighterAttrName,
|
|
17
|
+
skipHighlighterAttr,
|
|
18
|
+
skipHighlighterAttrName,
|
|
19
|
+
} from './ignore-highlighter';
|
|
20
|
+
|
|
21
|
+
export type { MatchRule } from './rule-matcher';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ComponentStrip } from './component-strip';
|
|
3
|
+
import { MockButton, MockSnap } from '../mock-component';
|
|
4
|
+
|
|
5
|
+
export const ComponentStripPreview = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div style={{ fontFamily: 'sans-serif', padding: 8 }}>
|
|
8
|
+
<ComponentStrip component={MockSnap} />
|
|
9
|
+
<br />
|
|
10
|
+
<ComponentStrip component={MockButton} />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
$borderRadius: 0.5em;
|
|
2
|
+
$gap: 0.125em;
|
|
3
|
+
|
|
4
|
+
.componentStrip {
|
|
5
|
+
display: flex;
|
|
6
|
+
width: fit-content; // for correct shadow size
|
|
7
|
+
|
|
8
|
+
border-radius: $borderRadius;
|
|
9
|
+
box-shadow: var(--bit-highlighter-shadow);
|
|
10
|
+
white-space: nowrap;
|
|
11
|
+
|
|
12
|
+
> * {
|
|
13
|
+
padding: 0 0.5em;
|
|
14
|
+
line-height: 1.5; //use line height to get rounder values than 0.25em padding
|
|
15
|
+
|
|
16
|
+
transition: filter 300ms, background-color 300ms;
|
|
17
|
+
transform: translateZ(0); //fix blurriness in Safari
|
|
18
|
+
|
|
19
|
+
background: var(--bit-highlighter-color, #eebcc9);
|
|
20
|
+
|
|
21
|
+
margin-right: $gap;
|
|
22
|
+
|
|
23
|
+
&:link,
|
|
24
|
+
&:visited {
|
|
25
|
+
text-decoration: inherit; // reset browser defaults
|
|
26
|
+
color: inherit; // reset browser defaults
|
|
27
|
+
|
|
28
|
+
&:hover {
|
|
29
|
+
background: var(--bit-highlighter-color-hover, #f6dae2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:active {
|
|
33
|
+
background: var(--bit-highlighter-color-active, #e79db1);
|
|
34
|
+
color: inherit;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:first-child {
|
|
39
|
+
border-top-left-radius: $borderRadius;
|
|
40
|
+
border-bottom-left-radius: $borderRadius;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&:last-child {
|
|
44
|
+
border-top-right-radius: $borderRadius;
|
|
45
|
+
border-bottom-right-radius: $borderRadius;
|
|
46
|
+
|
|
47
|
+
margin-right: unset;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.nameBlock {
|
|
53
|
+
display: flex;
|
|
54
|
+
|
|
55
|
+
.version {
|
|
56
|
+
// leave room for 9 digits + 3 "."
|
|
57
|
+
max-width: 13ch;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
text-overflow: ellipsis;
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
|
|
62
|
+
transition: max-width 480ms;
|
|
63
|
+
|
|
64
|
+
&:hover {
|
|
65
|
+
max-width: 61ch;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|