@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.
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 +113 -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 +14 -0
  34. package/dist/frame/frame.js +138 -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 +83 -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 +93 -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 +71 -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 +13 -0
  84. package/dist/label/label-container.js +87 -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 +70 -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-1768840768294.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 +142 -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 +142 -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 +74 -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 +60 -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,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,5 @@
1
+ export { useHoverHighlighter } from './use-hover-highlighter';
2
+ export type { useHoverHighlighterOptions } from './use-hover-highlighter';
3
+
4
+ export { HoverHighlighter } from './hover-highlighter';
5
+ export type { HoverHighlighterProps } from './hover-highlighter';
@@ -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,2 @@
1
+ export { HybridHighlighter } from './hybrid-highlighter';
2
+ export type { HybridHighlighterProps } from './hybrid-highlighter';
@@ -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
+ }