• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 25884645943

14 May 2026 08:43PM UTC coverage: 57.684% (-1.0%) from 58.684%
25884645943

push

github

web-flow
feat: basic annotations (#3434)

* feat: basic annotations

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* fixes and improvements

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fix annotations lag

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* tests, lint, fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* formatting/prettier

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* update icon from target to letters

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fix tests

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* fix dragging

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow up

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fixes; follow ups

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

---------

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Co-authored-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

7158 of 14867 branches covered (48.15%)

Branch coverage included in aggregate %.

217 of 737 new or added lines in 25 files covered. (29.44%)

70 existing lines in 2 files now uncovered.

14556 of 22776 relevant lines covered (63.91%)

77.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.7
/src/components/src/annotations/annotation-text.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import React, {FC, useCallback, useEffect, useMemo} from 'react';
5
import styled from 'styled-components';
6
import {useDraggable} from '@dnd-kit/core';
7
import {Annotation, AnnotationWithArm} from '@kepler.gl/types';
8
import {AnnotationKind} from '@kepler.gl/constants';
9
import {makeMarker, isLeftOriented, MapViewport} from './annotation-utils';
10
import {LexicalRichTextEditor, LexicalToolbar} from './lexical-editor';
11
import {SerializedEditorState} from 'lexical';
12

13
type StyledAnnotationTextProps = {
14
  $isEditing?: boolean;
15
  $isSelected?: boolean;
16
  $isEditingText?: boolean;
17
  $textVerticalAlign?: string;
18
};
19

20
const StyledAnnotationText = styled.div<StyledAnnotationTextProps>`
7✔
21
  position: absolute;
22
  color: #fff;
23
  transition: border-color 0.2s;
24
  border: 2px solid transparent;
25
  padding: 4px 8px;
26
  white-space: pre-wrap;
27
  word-break: break-word;
28
  &:focus {
29
    outline: none;
30
  }
31
  ${props =>
NEW
32
    props.$isEditing
×
33
      ? `
34
        border-color: ${props.$isSelected ? 'rgba(255,255,255,0.8)' : 'rgba(255,255,255,0.2)'};
×
35
        &:hover {
36
          border-color: rgba(255,255,255,0.6);
37
        }
38
        cursor: move;
39
        pointer-events: all;
40
      `
41
      : `
42
        pointer-events: none;
43
        a {
44
          pointer-events: all;
45
          cursor: pointer;
46
        }
47
      `}
48
  ${props =>
NEW
49
    props.$isEditingText
×
50
      ? `
51
    .editor-inner {
52
      cursor: text;
53
    }
54
  `
55
      : ''}
56
  .editor-inner {
57
    justify-content: ${props =>
NEW
58
      props.$textVerticalAlign === 'top'
×
59
        ? 'flex-start'
60
        : props.$textVerticalAlign === 'middle'
×
61
        ? 'center'
62
        : 'flex-end'};
63
  }
64
`;
65

66
const StyledToolbarWrapper = styled.div`
7✔
67
  position: absolute;
68
  bottom: 100%;
69
  left: 0;
70
  background-color: rgba(30, 33, 40, 0.95);
71
  border-radius: 5px;
72
  width: max-content;
73
  padding: 2px 5px;
74
  margin-bottom: 4px;
75
  z-index: 1;
76
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
77
`;
78

79
export type AnnotationTextProps = {
80
  annotation: Annotation;
81
  viewport: MapViewport;
82
  isEditing: boolean;
83
  isSelected: boolean;
84
  isEditingText: boolean;
85
  onSelect: (isEditingText: boolean) => void;
86
  onChangeText: (text: string, editorState?: SerializedEditorState) => void;
87
  onUpdateAnnotation: (config: Partial<Annotation>) => void;
88
};
89

90
const AnnotationText: FC<AnnotationTextProps> = ({
7✔
91
  annotation,
92
  viewport,
93
  isEditing,
94
  isSelected,
95
  isEditingText,
96
  onSelect,
97
  onChangeText,
98
  onUpdateAnnotation
99
}) => {
NEW
100
  const {attributes, listeners, setNodeRef} = useDraggable({
×
101
    id: `${annotation.id}:MOVE_TEXT`
102
  });
103

104
  const {textWidth, textHeight, lineColor, lineWidth, textVerticalAlign, autoSize, kind} =
NEW
105
    annotation;
×
NEW
106
  const {x, y, tx, ty} = makeMarker(annotation, viewport);
×
107

NEW
108
  const isArm = 'armLength' in annotation;
×
NEW
109
  const isLeft = isArm && isLeftOriented((annotation as AnnotationWithArm).angle);
×
110

NEW
111
  const style = useMemo(
×
NEW
112
    () => ({
×
113
      ...(autoSize ? {minWidth: 80} : {width: textWidth || 120}),
×
114
      bottom: viewport.height - (y + ty),
115
      borderBottom:
×
116
        kind !== AnnotationKind.TEXT ? `${lineWidth}px solid ${lineColor}` : undefined,
×
117
      ...(kind === AnnotationKind.TEXT
×
118
        ? {left: x + tx - (textWidth || 80) / 2}
×
119
        : isLeft
120
        ? {right: viewport.width - x - tx}
121
        : {left: x + tx})
122
    }),
123
    [autoSize, kind, textWidth, tx, ty, viewport.width, viewport.height, x, y, isLeft, lineWidth, lineColor]
124
  );
125

126
  const handleEditorChange = useCallback(
127
    (change: {text: string; editorState: SerializedEditorState}) => {
128
      onChangeText(change.text, change.editorState);
129
    },
130
    [onChangeText]
131
  );
132

133
  useEffect(() => {
134
    if (!isEditingText || !isSelected || !isEditing) {
135
      window.getSelection()?.removeAllRanges();
136
    }
137
  }, [isEditingText, isSelected, isEditing]);
NEW
138

×
139
  return (
NEW
140
    <StyledAnnotationText
×
141
      ref={setNodeRef}
142
      $isEditing={isEditing}
143
      $isEditingText={isEditingText}
144
      $isSelected={isSelected}
NEW
145
      $textVerticalAlign={textVerticalAlign}
×
NEW
146
      style={style}
×
NEW
147
      {...(isEditing
×
148
        ? {
149
            ...attributes,
150
            ...listeners,
NEW
151
            onPointerDown: (evt: React.PointerEvent) => {
×
152
              const {target} = evt;
153
              if (target instanceof Element && target.closest('.lexical-toolbar')) {
154
                return;
155
              }
156
              if (isEditingText) {
157
                if (target instanceof Element && target.closest('.editor-inner')) {
158
                  return;
159
                }
×
160
                listeners?.onPointerDown?.(evt as any);
161
                return;
162
              }
163
              listeners?.onPointerDown?.(evt as any);
NEW
164
              if (!isSelected) {
×
NEW
165
                onSelect(false);
×
NEW
166
              }
×
167
            }
NEW
168
          }
×
NEW
169
        : {})}
×
NEW
170
      onClick={evt => {
×
171
        if (!isEditing) return;
NEW
172
        const target = evt.target;
×
NEW
173
        if (target instanceof Element && target.closest('.lexical-toolbar')) {
×
174
          return;
NEW
175
        }
×
NEW
176
        evt.stopPropagation();
×
NEW
177
        if (!isEditingText) {
×
178
          onSelect(true);
179
        }
180
      }}
181
      onDoubleClick={() => {
182
        if (!isEditing) {
NEW
183
          onSelect(true);
×
NEW
184
        }
×
NEW
185
      }}
×
NEW
186
    >
×
187
      <LexicalRichTextEditor
NEW
188
        {...(isEditingText && annotation.autoSize ? {} : {width: textWidth || undefined})}
×
NEW
189
        {...(isEditingText && annotation.autoSizeY ? {} : {height: textHeight || undefined})}
×
NEW
190
        isEditable={isEditing && isEditingText}
×
191
        editorState={annotation.editorState as SerializedEditorState | undefined}
192
        initialText={annotation.label}
193
        onChange={handleEditorChange}
NEW
194
      >
×
NEW
195
        {isEditing && isSelected ? (
×
196
          <StyledToolbarWrapper>
197
            <LexicalToolbar
198
              isEditingText={isEditingText}
199
              textVerticalAlign={textVerticalAlign}
200
              onChangeTextVerticalAlign={value => {
×
201
                onUpdateAnnotation({textVerticalAlign: value});
×
202
              }}
×
203
            />
204
          </StyledToolbarWrapper>
205
        ) : null}
206
      </LexicalRichTextEditor>
207
    </StyledAnnotationText>
×
208
  );
209
};
210

211
export default AnnotationText;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc