Skip to content

Commit 2cfe5d1

Browse files
authored
Merge pull request #2261 from pie-framework/develop
fix: PIE-661 PIE-664 PIE-683 PIE-666 PIE-656 PIE-657 PIE-658 PIE-659 PIE-670
2 parents 3ab0bf1 + 8d80ab6 commit 2cfe5d1

11 files changed

Lines changed: 321 additions & 137 deletions

File tree

packages/charting/src/mark-label.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const StyledInput = styled('input')(({ theme }) => ({
2323
border: 'none',
2424
'&.correct': correct('color'),
2525
'&.incorrect': incorrect('color'),
26-
'&.disabled': {
26+
'&.disabledMarkLabel': {
2727
backgroundColor: 'transparent !important',
2828
},
2929
'&.error': { border: `2px solid ${theme.palette.error.main}` },
@@ -36,7 +36,7 @@ const StyledMathInput = styled('div')(({ theme }) => ({
3636
fontFamily: theme.typography.fontFamily,
3737
color: color.primaryDark(),
3838
paddingTop: theme.typography.fontSize / 2,
39-
'&.disabled': {
39+
'&.disabledMarkLabel': {
4040
...disabled('color'),
4141
backgroundColor: 'transparent !important',
4242
},
@@ -155,7 +155,7 @@ export const MarkLabel = (props) => {
155155
}
156156
}}
157157
className={classNames({
158-
disabled: disabled,
158+
disabledMarkLabel: disabled,
159159
error: error,
160160
correct: mark.editable && correctness?.label === 'correct',
161161
incorrect: mark.editable && correctness?.label === 'incorrect',
@@ -183,7 +183,7 @@ export const MarkLabel = (props) => {
183183
disabled={disabled}
184184
inputClassName={classNames(
185185
correctness && mark.editable ? correctness.label : null,
186-
disabled && 'disabled',
186+
disabled && 'disabledMarkLabel',
187187
error && 'error',
188188
)}
189189
inputStyle={{

packages/drag/src/__tests__/placeholder.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('placeholder', () => {
2929
it('applies disabled class when disabled is true', () => {
3030
const { container } = renderComponent({ disabled: true });
3131
const placeholder = container.firstChild;
32-
expect(placeholder).toHaveClass('disabled');
32+
expect(placeholder).toHaveClass('placeholderDisabled');
3333
});
3434

3535
it('applies custom className', () => {

packages/drag/src/placeholder.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const StyledPlaceholder = styled('div')(({ theme }) => ({
2424
padding: theme.spacing(1),
2525
border: `2px dashed ${color.black()}`,
2626
},
27-
'&.disabled': {
27+
'&.placeholderDisabled': {
2828
boxShadow: 'none',
2929
background: theme.palette.background.paper,
3030
},
@@ -75,7 +75,13 @@ export const PlaceHolder = (props) => {
7575
extraStyles,
7676
} = props;
7777

78-
const names = classNames('placeholder', disabled && 'disabled', isOver && 'over', type, className);
78+
const names = classNames(
79+
'placeholder',
80+
disabled && 'placeholderDisabled',
81+
isOver && 'over',
82+
type,
83+
className,
84+
);
7985

8086
const style = {};
8187

packages/editable-html-tip-tap/src/components/CharacterPicker.jsx

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,52 @@ export function CharacterPicker({ editor, opts, onClose }) {
7878
useEffect(() => {
7979
if (!editor) return;
8080

81-
// Calculate position relative to selection
8281
const editorDOM = editor.options.element;
83-
const editorRect = editorDOM.getBoundingClientRect();
84-
const bodyRect = document.body.getBoundingClientRect();
85-
const { from } = editor.state.selection;
86-
const start = editor.view.coordsAtPos(from);
82+
const editorViewDom = editor.view.dom;
8783

88-
let top = editorRect.top + Math.abs(bodyRect.top) + editorRect.height + 60;
84+
// Position is computed in viewport coordinates (the dialog uses position: fixed),
85+
// so coordsAtPos / getBoundingClientRect values can be used directly without
86+
// adding scroll offsets. The dialog is then clamped to the viewport so it does
87+
// not get cut off by fixed page headers/footers.
88+
const updatePosition = () => {
89+
if (!containerRef.current) return;
8990

90-
if (editorRect.y > containerRef.current.offsetHeight) {
91-
top = top - (containerRef.current.offsetHeight + editorRect.height) - 80;
92-
}
91+
const editorRect = editorDOM.getBoundingClientRect();
92+
const { from } = editor.state.selection;
93+
const start = editor.view.coordsAtPos(from);
9394

94-
setPosition({
95-
top: top,
96-
left: start.left,
97-
});
95+
const dialogHeight = containerRef.current.offsetHeight;
96+
const dialogWidth = containerRef.current.offsetWidth;
9897

99-
const editorViewDom = editor.view.dom;
98+
// prefer below the editor; flip above when there isn't room below.
99+
const spaceBelow = window.innerHeight - (editorRect.bottom + 60);
100+
let top =
101+
spaceBelow >= dialogHeight || editorRect.top < dialogHeight + 80
102+
? editorRect.bottom + 60
103+
: editorRect.top - dialogHeight - 20;
104+
105+
let left = start.left;
106+
107+
const margin = 8;
108+
top = Math.max(margin, Math.min(top, window.innerHeight - dialogHeight - margin));
109+
left = Math.max(margin, Math.min(left, window.innerWidth - dialogWidth - margin));
110+
111+
setPosition({ top, left });
112+
};
113+
114+
updatePosition();
115+
116+
let frame = null;
117+
const scheduleUpdate = () => {
118+
if (frame !== null) return;
119+
frame = requestAnimationFrame(() => {
120+
frame = null;
121+
updatePosition();
122+
});
123+
};
124+
125+
window.addEventListener('scroll', scheduleUpdate, true);
126+
window.addEventListener('resize', scheduleUpdate);
100127

101128
const handleClickOutside = (e) => {
102129
if (containerRef.current && !containerRef.current.contains(e.target) && !editorViewDom.contains(e.target)) {
@@ -110,6 +137,9 @@ export function CharacterPicker({ editor, opts, onClose }) {
110137

111138
return () => {
112139
clearTimeout(timeoutId);
140+
if (frame !== null) cancelAnimationFrame(frame);
141+
window.removeEventListener('scroll', scheduleUpdate, true);
142+
window.removeEventListener('resize', scheduleUpdate);
113143
document.removeEventListener('click', handleClickOutside);
114144
};
115145
}, [editor]);
@@ -131,11 +161,11 @@ export function CharacterPicker({ editor, opts, onClose }) {
131161
data-toolbar-for={editor.instanceId}
132162
style={{
133163
visibility: position.top === 0 && position.left === 0 ? 'hidden' : 'initial',
134-
position: 'absolute',
164+
position: 'fixed',
135165
top: `${position.top}px`,
136166
left: `${position.left}px`,
137167
maxWidth: '500px',
138-
zIndex: 99,
168+
zIndex: 1000,
139169
}}
140170
>
141171
<div>

packages/editable-html-tip-tap/src/components/__tests__/CharacterPicker.test.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,16 @@ describe('CharacterPicker', () => {
234234
};
235235
const { container } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
236236
const dialog = container.querySelector('.insert-character-dialog');
237-
expect(dialog).toHaveStyle({ position: 'absolute' });
237+
expect(dialog).toHaveStyle({ position: 'fixed' });
238+
});
239+
240+
it('renders above other editor overlays with a high z-index', () => {
241+
const opts = {
242+
characters: [['á']],
243+
};
244+
const { container } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
245+
const dialog = container.querySelector('.insert-character-dialog');
246+
expect(dialog).toHaveStyle({ zIndex: '1000' });
238247
});
239248

240249
it('adds data-toolbar-for attribute with editor instanceId', () => {

packages/editable-html-tip-tap/src/components/__tests__/InlineDropdown.test.jsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ jest.mock('@tiptap/react', () => ({
1010
),
1111
}));
1212

13+
const mockCreatePortal = jest.fn((node) => node);
14+
1315
jest.mock('react-dom', () => ({
1416
...jest.requireActual('react-dom'),
15-
createPortal: (node) => node,
17+
createPortal: (...args) => mockCreatePortal(...args),
1618
}));
1719

1820
describe('InlineDropdown', () => {
@@ -77,6 +79,7 @@ describe('InlineDropdown', () => {
7779

7880
beforeEach(() => {
7981
jest.clearAllMocks();
82+
mockCreatePortal.mockClear();
8083
mockEditor = buildMockEditor();
8184
defaultProps.editor = mockEditor;
8285
Object.defineProperty(document.body, 'getBoundingClientRect', {
@@ -211,14 +214,29 @@ describe('InlineDropdown', () => {
211214
expect(textContainer).toHaveStyle({ textOverflow: 'ellipsis', whiteSpace: 'nowrap' });
212215
});
213216

214-
it('renders toolbar with z-index', async () => {
215-
const { container } = render(<InlineDropdown {...defaultProps} selected={true} />);
217+
it('portals toolbar into editor container when _tiptapContainerEl is set', async () => {
218+
const containerEl = document.createElement('div');
219+
const editor = buildMockEditor({ _tiptapContainerEl: containerEl });
220+
221+
render(<InlineDropdown {...defaultProps} editor={editor} selected={true} />);
222+
223+
await waitFor(() => {
224+
expect(mockCreatePortal).toHaveBeenCalled();
225+
});
226+
227+
expect(mockCreatePortal.mock.calls[0][1]).toBe(containerEl);
228+
});
229+
230+
it('portals toolbar into document.body when _tiptapContainerEl is missing', async () => {
231+
const editor = buildMockEditor({ _tiptapContainerEl: undefined });
232+
233+
render(<InlineDropdown {...defaultProps} editor={editor} selected={true} />);
234+
216235
await waitFor(() => {
217-
const toolbarContainer = container.querySelector('div[style*="zIndex"]');
218-
if (toolbarContainer) {
219-
expect(toolbarContainer).toHaveStyle({ zIndex: '1' });
220-
}
236+
expect(mockCreatePortal).toHaveBeenCalled();
221237
});
238+
239+
expect(mockCreatePortal.mock.calls[0][1]).toBe(document.body);
222240
});
223241

224242
it('passes editorCallback to InlineDropdownToolbar', async () => {

packages/editable-html-tip-tap/src/components/common/toolbar-buttons.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const StyledButton = styled('button', {
1212
background: 'none',
1313
border: 'none',
1414
cursor: disabled ? 'not-allowed' : 'pointer',
15+
// previously we had implicit 24×24 icon rendering for mui svg icons, but now we need to explicitly set the size to 24×24 to match the previous behavior
16+
'& svg': {
17+
width: '24px',
18+
height: '24px',
19+
},
1520
'&:hover': {
1621
color: disabled ? 'grey' : 'black',
1722
},

packages/editable-html-tip-tap/src/components/respArea/InlineDropdown.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,14 @@ const InlineDropdown = (props) => {
175175
{showToolbar && (
176176
<React.Fragment>
177177
{ReactDOM.createPortal(
178-
<div ref={toolbarRef} style={{ zIndex: 1 }}>
178+
<div ref={toolbarRef}>
179179
<InlineDropdownToolbar
180180
editorCallback={(instance) => {
181181
toolbarEditor.current = instance;
182182
}}
183183
/>
184184
</div>,
185-
document.body,
185+
editor?._tiptapContainerEl || document.body,
186186
)}
187187

188188
{editor._tiptapContainerEl &&

0 commit comments

Comments
 (0)