@rpg-engine/long-bow 0.8.235 → 0.8.236
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/dist/components/Spellbook/Spell.d.ts +2 -1
- package/dist/components/Spellbook/Spellbook.d.ts +2 -1
- package/dist/components/Spellbook/cards/SpellTooltip.d.ts +1 -0
- package/dist/long-bow.cjs.development.js +561 -200
- package/dist/long-bow.cjs.development.js.map +1 -1
- package/dist/long-bow.cjs.production.min.js +1 -1
- package/dist/long-bow.cjs.production.min.js.map +1 -1
- package/dist/long-bow.esm.js +562 -201
- package/dist/long-bow.esm.js.map +1 -1
- package/package.json +13 -3
- package/src/components/Shortcuts/ShortcutsSetter.tsx +6 -3
- package/src/components/Spellbook/Spell.tsx +195 -234
- package/src/components/Spellbook/Spellbook.tsx +407 -111
- package/src/components/Spellbook/__tests__/SpellInfoWrapper.test.tsx +102 -0
- package/src/components/Spellbook/__tests__/Spellbook.test.tsx +113 -0
- package/src/components/Spellbook/__tests__/imageMock.js +1 -0
- package/src/components/Spellbook/cards/SpellInfo.tsx +84 -29
- package/src/components/Spellbook/cards/SpellInfoWrapper.tsx +48 -13
- package/src/components/Spellbook/cards/SpellTooltip.tsx +13 -6
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render, screen, cleanup } from '@testing-library/react';
|
|
3
|
+
import { BasicAttribute, IRawSpell, ISpell, ShortcutType, SpellsBlueprint } from '@rpg-engine/shared';
|
|
4
|
+
import { Spellbook, ISpellbookProps } from '../Spellbook';
|
|
5
|
+
import { Spell } from '../Spell';
|
|
6
|
+
import { mockSpells } from '../mockSpells';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../shared/SpriteFromAtlas', () => ({ SpriteFromAtlas: () => null }));
|
|
9
|
+
jest.mock('../cards/SpellInfoWrapper', () => ({ SpellInfoWrapper: ({ children }: any) => children }));
|
|
10
|
+
afterEach(cleanup);
|
|
11
|
+
|
|
12
|
+
const spells: ISpell[] = Array.from({ length: 9 }, (_, i) => ({
|
|
13
|
+
...mockSpells[0], key: `spell-${i}` as SpellsBlueprint,
|
|
14
|
+
name: `Spell ${i}`, magicWords: `words ${i}`, minMagicLevelRequired: 9 - i,
|
|
15
|
+
}));
|
|
16
|
+
const props: ISpellbookProps = {
|
|
17
|
+
spells, mana: 200, magicLevel: 100, shortcuts: [], atlasIMG: '', atlasJSON: {},
|
|
18
|
+
onSpellClick: jest.fn(), setSpellShortcut: jest.fn(), removeShortcut: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it('paginates without mutating the caller array and resets search to the first page', () => {
|
|
22
|
+
const order = spells.map(spell => spell.key);
|
|
23
|
+
render(<Spellbook {...props} />);
|
|
24
|
+
expect(spells.map(spell => spell.key)).toEqual(order);
|
|
25
|
+
expect(screen.getAllByRole('button', { name: /^Cast Spell/ })).toHaveLength(8);
|
|
26
|
+
fireEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
27
|
+
expect(screen.getAllByRole('button', { name: /^Cast Spell/ })).toHaveLength(1);
|
|
28
|
+
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'words 8' } });
|
|
29
|
+
expect(screen.getByRole('button', { name: 'Cast Spell 8' })).toBeTruthy();
|
|
30
|
+
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'missing' } });
|
|
31
|
+
expect(screen.getByText('No matching spells')).toBeTruthy();
|
|
32
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear search' }));
|
|
33
|
+
expect(screen.getAllByRole('button', { name: /^Cast Spell/ })).toHaveLength(8);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('keeps a valid page when the learned spell list shrinks', () => {
|
|
37
|
+
const { rerender } = render(<Spellbook {...props} />);
|
|
38
|
+
fireEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
39
|
+
rerender(<Spellbook {...props} spells={spells.slice(0, 2)} />);
|
|
40
|
+
expect(screen.getAllByRole('button', { name: /^Cast Spell/ })).toHaveLength(2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('assigns a shortcut without casting and supports cancellation', () => {
|
|
44
|
+
const setSpellShortcut = jest.fn();
|
|
45
|
+
const onSpellClick = jest.fn();
|
|
46
|
+
render(<Spellbook {...props} onSpellClick={onSpellClick} setSpellShortcut={setSpellShortcut} />);
|
|
47
|
+
fireEvent.click(screen.getByRole('button', { name: 'Set shortcut 1' }));
|
|
48
|
+
fireEvent.click(screen.getByRole('button', { name: 'Assign Spell 8' }));
|
|
49
|
+
expect(setSpellShortcut).toHaveBeenCalledWith('spell-8', 0);
|
|
50
|
+
expect(onSpellClick).not.toHaveBeenCalled();
|
|
51
|
+
fireEvent.click(screen.getByRole('button', { name: 'Set shortcut 1' }));
|
|
52
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cancel assignment' }));
|
|
53
|
+
expect(screen.getByRole('button', { name: 'Cast Spell 8' })).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('blocks cast on low skills, mana and cooldown but permits assignment without mana', () => {
|
|
57
|
+
const onClick = jest.fn();
|
|
58
|
+
const spell = { ...spells[0], attribute: BasicAttribute.Strength, minSkillLevelRequired: 10 };
|
|
59
|
+
const card = { atlasIMG: '', atlasJSON: {}, spellKey: spell.key, spell, charMagicLevel: 100, charMana: 200, onClick };
|
|
60
|
+
const { rerender } = render(<Spell {...card} charSkillLevels={{ strength: 2 }} />);
|
|
61
|
+
fireEvent.click(screen.getByRole('button'));
|
|
62
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
63
|
+
rerender(<Spell {...card} charSkillLevels={{ strength: 20 }} activeCooldown={2} />);
|
|
64
|
+
fireEvent.click(screen.getByRole('button'));
|
|
65
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
66
|
+
rerender(<Spell {...card} charSkillLevels={{ strength: 20 }} charMana={0} />);
|
|
67
|
+
fireEvent.click(screen.getByRole('button'));
|
|
68
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
69
|
+
rerender(<Spell {...card} charSkillLevels={{ strength: 20 }} charMana={0} isSettingShortcut />);
|
|
70
|
+
fireEvent.click(screen.getByRole('button'));
|
|
71
|
+
expect(onClick).toHaveBeenCalledWith(spell.key);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('clears an occupied shortcut without starting assignment', () => {
|
|
75
|
+
const shortcuts = [{ type: ShortcutType.Spell, payload: spells[0] as IRawSpell }];
|
|
76
|
+
const removeShortcut = (index: number) => { shortcuts.splice(index, 1); };
|
|
77
|
+
const view = render(<Spellbook {...props} shortcuts={shortcuts} removeShortcut={removeShortcut} />);
|
|
78
|
+
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: 'Clear shortcut 1' }));
|
|
80
|
+
expect(screen.queryByText('Choose a spell for shortcut 1')).toBeNull();
|
|
81
|
+
|
|
82
|
+
view.rerender(<Spellbook {...props} shortcuts={shortcuts} removeShortcut={removeShortcut} />);
|
|
83
|
+
fireEvent.click(screen.getByRole('button', { name: 'Set shortcut 1' }));
|
|
84
|
+
expect(screen.getByText('Choose a spell for shortcut 1')).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('spells out requirement, mana cost and cooldown and only prompts when assigning', () => {
|
|
88
|
+
const spell = { ...spells[0], attribute: BasicAttribute.MagicResistance, minSkillLevelRequired: 4, manaCost: 13, cooldown: 5 };
|
|
89
|
+
const card = { atlasIMG: '', atlasJSON: {}, spellKey: spell.key, spell, charMagicLevel: 100, charMana: 200, onClick: jest.fn() };
|
|
90
|
+
const ready = { ...card, charSkillLevels: { magicResistance: 9 } };
|
|
91
|
+
const { rerender } = render(<Spell {...ready} />);
|
|
92
|
+
expect(screen.getByText('Requires Magic Resistance 4')).toBeTruthy();
|
|
93
|
+
expect(screen.getByText('Mana cost')).toBeTruthy();
|
|
94
|
+
expect(screen.getByText('Cooldown')).toBeTruthy();
|
|
95
|
+
expect(screen.queryByText(/Select to/)).toBeNull();
|
|
96
|
+
const accessibleDescription = () => screen.getByRole('button').getAttribute('aria-describedby')!
|
|
97
|
+
.split(' ').map(id => document.getElementById(id)?.textContent).join(' ');
|
|
98
|
+
expect(accessibleDescription()).toContain(spell.description);
|
|
99
|
+
expect(accessibleDescription()).toContain('Requires Magic Resistance 4');
|
|
100
|
+
expect(accessibleDescription()).toContain('Mana cost');
|
|
101
|
+
|
|
102
|
+
rerender(<Spell {...ready} isSettingShortcut />);
|
|
103
|
+
expect(screen.getByText('Select to assign')).toBeTruthy();
|
|
104
|
+
rerender(<Spell {...ready} charMana={0} />);
|
|
105
|
+
expect(screen.getByText('Not enough mana')).toBeTruthy();
|
|
106
|
+
expect(accessibleDescription()).toContain('Not enough mana');
|
|
107
|
+
rerender(<Spell {...ready} activeCooldown={2} />);
|
|
108
|
+
expect(screen.getByText('Ready in 2.0s')).toBeTruthy();
|
|
109
|
+
expect(accessibleDescription()).toContain('Ready in 2.0s');
|
|
110
|
+
rerender(<Spell {...card} charSkillLevels={{ magicResistance: 1 }} />);
|
|
111
|
+
expect(screen.getAllByText('Requires Magic Resistance 4')).toHaveLength(1);
|
|
112
|
+
expect((screen.getByRole('button') as HTMLButtonElement).disabled).toBe(true);
|
|
113
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = 'book.png';
|
|
@@ -9,6 +9,13 @@ interface ISpellInfoProps {
|
|
|
9
9
|
spell: ISpell;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// "magicResistance" -> "Magic Resistance", "fire" -> "Fire"
|
|
13
|
+
const humanize = (value: string): string =>
|
|
14
|
+
formatSpellCastingType(value.replace(/([a-z])([A-Z])/g, '$1 $2'));
|
|
15
|
+
|
|
16
|
+
const humanizeList = (values: string[]): string =>
|
|
17
|
+
values.map(humanize).join(', ');
|
|
18
|
+
|
|
12
19
|
export const SpellInfo: React.FC<ISpellInfoProps> = ({ spell }) => {
|
|
13
20
|
const {
|
|
14
21
|
magicWords,
|
|
@@ -19,7 +26,46 @@ export const SpellInfo: React.FC<ISpellInfoProps> = ({ spell }) => {
|
|
|
19
26
|
castingType,
|
|
20
27
|
cooldown,
|
|
21
28
|
maxDistanceGrid,
|
|
29
|
+
minLevelRequired,
|
|
30
|
+
minMagicLevelRequired,
|
|
31
|
+
minSkillLevelRequired,
|
|
32
|
+
attribute,
|
|
33
|
+
elementalType,
|
|
34
|
+
canSelfTarget,
|
|
35
|
+
isPVPOnly,
|
|
36
|
+
characterClass,
|
|
37
|
+
characterRace,
|
|
38
|
+
onlyPremiumAccountType,
|
|
22
39
|
} = spell;
|
|
40
|
+
|
|
41
|
+
// Same gate as the card and the API: a skill attribute replaces magic level.
|
|
42
|
+
const skill =
|
|
43
|
+
attribute && minSkillLevelRequired !== undefined
|
|
44
|
+
? { label: humanize(attribute), level: minSkillLevelRequired }
|
|
45
|
+
: { label: 'Magic', level: minMagicLevelRequired };
|
|
46
|
+
|
|
47
|
+
// Only fields present in the payload are listed. Nothing is defaulted, and an
|
|
48
|
+
// empty restriction list is not shown as a restriction.
|
|
49
|
+
const details: Array<[string, string | number]> = [];
|
|
50
|
+
if (elementalType) details.push(['Element:', humanize(elementalType)]);
|
|
51
|
+
if (canSelfTarget !== undefined) {
|
|
52
|
+
details.push(['Can target self:', canSelfTarget ? 'Yes' : 'No']);
|
|
53
|
+
}
|
|
54
|
+
if (isPVPOnly) details.push(['PvP only:', 'Yes']);
|
|
55
|
+
if (minLevelRequired > 0) details.push(['Requires Level:', minLevelRequired]);
|
|
56
|
+
if (skill.level > 0) {
|
|
57
|
+
details.push([`Requires ${skill.label} Level:`, skill.level]);
|
|
58
|
+
}
|
|
59
|
+
if (characterClass?.length) {
|
|
60
|
+
details.push(['Requires Class:', humanizeList(characterClass)]);
|
|
61
|
+
}
|
|
62
|
+
if (characterRace?.length) {
|
|
63
|
+
details.push(['Requires Race:', humanizeList(characterRace)]);
|
|
64
|
+
}
|
|
65
|
+
if (onlyPremiumAccountType?.length) {
|
|
66
|
+
details.push(['Eligible account types:', humanizeList(onlyPremiumAccountType)]);
|
|
67
|
+
}
|
|
68
|
+
|
|
23
69
|
return (
|
|
24
70
|
<Container>
|
|
25
71
|
<Header>
|
|
@@ -50,6 +96,12 @@ export const SpellInfo: React.FC<ISpellInfoProps> = ({ spell }) => {
|
|
|
50
96
|
<div className="value">{maxDistanceGrid}</div>
|
|
51
97
|
</Statistic>
|
|
52
98
|
)}
|
|
99
|
+
{details.map(([label, value]) => (
|
|
100
|
+
<Statistic key={label}>
|
|
101
|
+
<div className="label">{label}</div>
|
|
102
|
+
<div className="value">{value}</div>
|
|
103
|
+
</Statistic>
|
|
104
|
+
))}
|
|
53
105
|
<Statistic>
|
|
54
106
|
{requiredItem && (
|
|
55
107
|
<>
|
|
@@ -64,62 +116,65 @@ export const SpellInfo: React.FC<ISpellInfoProps> = ({ spell }) => {
|
|
|
64
116
|
};
|
|
65
117
|
|
|
66
118
|
const Container = styled.div`
|
|
67
|
-
|
|
68
|
-
background-color: #222;
|
|
69
|
-
border-radius: 5px;
|
|
70
|
-
padding: 0.5rem;
|
|
71
|
-
font-size: ${uiFonts.size.small};
|
|
72
|
-
border: 3px solid ${uiColors.lightGray};
|
|
119
|
+
box-sizing: border-box;
|
|
73
120
|
height: max-content;
|
|
74
|
-
width: 30rem;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
121
|
+
width: min(30rem, 86vw);
|
|
122
|
+
padding: 12px 14px;
|
|
123
|
+
color: #f4e2bb;
|
|
124
|
+
background: linear-gradient(#3f2815, #2c1b0d);
|
|
125
|
+
border: 3px double #c99a52;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.55);
|
|
128
|
+
font-family: 'Press Start 2P', monospace;
|
|
129
|
+
font-size: ${uiFonts.size.small};
|
|
130
|
+
line-height: 1.7;
|
|
79
131
|
`;
|
|
80
132
|
|
|
81
133
|
const Title = styled.div`
|
|
82
|
-
font-size: ${uiFonts.size.medium};
|
|
83
|
-
font-weight: bold;
|
|
84
|
-
margin-bottom: 0.5rem;
|
|
85
|
-
display: flex;
|
|
86
|
-
align-items: center;
|
|
87
134
|
margin: 0;
|
|
135
|
+
font-size: ${uiFonts.size.medium};
|
|
136
|
+
color: #ffe3a3;
|
|
88
137
|
`;
|
|
89
138
|
|
|
90
139
|
const Type = styled.div`
|
|
140
|
+
margin-top: 4px;
|
|
91
141
|
font-size: ${uiFonts.size.small};
|
|
92
|
-
|
|
93
|
-
color: ${uiColors.lightGray};
|
|
142
|
+
color: #d9b878;
|
|
94
143
|
`;
|
|
95
144
|
|
|
96
145
|
const Description = styled.div`
|
|
97
|
-
margin-top:
|
|
146
|
+
margin-top: 12px;
|
|
147
|
+
padding-top: 10px;
|
|
148
|
+
border-top: 1px solid rgba(244, 226, 187, 0.3);
|
|
98
149
|
font-size: ${uiFonts.size.small};
|
|
99
|
-
color:
|
|
100
|
-
font-style: italic;
|
|
150
|
+
color: #e8d5ab;
|
|
101
151
|
`;
|
|
102
152
|
|
|
103
153
|
const Header = styled.div`
|
|
104
154
|
display: flex;
|
|
105
155
|
align-items: center;
|
|
106
156
|
justify-content: space-between;
|
|
107
|
-
margin-bottom:
|
|
157
|
+
margin-bottom: 10px;
|
|
108
158
|
`;
|
|
109
159
|
|
|
110
160
|
const Statistic = styled.div`
|
|
111
|
-
|
|
112
|
-
|
|
161
|
+
display: flex;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
gap: 16px;
|
|
164
|
+
padding: 3px 0;
|
|
165
|
+
border-bottom: 1px dotted rgba(244, 226, 187, 0.22);
|
|
166
|
+
|
|
167
|
+
&:empty {
|
|
168
|
+
display: none;
|
|
169
|
+
}
|
|
113
170
|
|
|
114
171
|
.label {
|
|
115
|
-
|
|
116
|
-
margin-right: 0.5rem;
|
|
117
|
-
color: inherit;
|
|
172
|
+
color: #cdb48a;
|
|
118
173
|
}
|
|
119
174
|
|
|
120
175
|
.value {
|
|
121
|
-
|
|
122
|
-
|
|
176
|
+
color: #f4e2bb;
|
|
177
|
+
text-align: right;
|
|
123
178
|
}
|
|
124
179
|
|
|
125
180
|
&.better,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ISpell } from '@rpg-engine/shared';
|
|
2
|
-
import React, { useRef, useState } from 'react';
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { MobileSpellTooltip } from './MobileSpellTooltip';
|
|
4
4
|
import { MagicTooltip } from './SpellTooltip';
|
|
5
5
|
|
|
@@ -19,40 +19,75 @@ export const SpellInfoWrapper: React.FC<ISpellInfoWrapperProps> = ({
|
|
|
19
19
|
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
|
|
20
20
|
const [isTooltipMobileVisible, setIsTooltipMobileVisible] = useState(false);
|
|
21
21
|
const touchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
22
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const suppressClickRef = useRef(false);
|
|
25
|
+
const clearHold = (): void => {
|
|
26
|
+
if (touchTimeoutRef.current) clearTimeout(touchTimeoutRef.current);
|
|
27
|
+
touchTimeoutRef.current = null;
|
|
28
|
+
};
|
|
29
|
+
useEffect(() => clearHold, []);
|
|
30
|
+
|
|
31
|
+
const handleTouchStart = (): void => {
|
|
32
|
+
clearHold();
|
|
33
|
+
suppressClickRef.current = false;
|
|
24
34
|
touchTimeoutRef.current = setTimeout(() => {
|
|
35
|
+
touchTimeoutRef.current = null;
|
|
36
|
+
suppressClickRef.current = true;
|
|
25
37
|
setIsTooltipMobileVisible(true);
|
|
26
38
|
setIsTooltipVisible(false);
|
|
27
39
|
}, holdTouchDuration);
|
|
28
40
|
};
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
if (touchTimeoutRef.current) {
|
|
32
|
-
clearTimeout(touchTimeoutRef.current);
|
|
33
|
-
touchTimeoutRef.current = null;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
42
|
+
|
|
36
43
|
|
|
37
44
|
return (
|
|
38
45
|
<div
|
|
46
|
+
ref={wrapperRef}
|
|
47
|
+
onFocus={event => {
|
|
48
|
+
if (!isTooltipMobileVisible && (event.target as HTMLElement).matches(':focus-visible')) {
|
|
49
|
+
setIsTooltipVisible(true);
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
onBlur={event => {
|
|
53
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
|
54
|
+
setIsTooltipVisible(false);
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
onKeyDown={event => {
|
|
58
|
+
if (event.key === 'Escape' && isTooltipVisible) {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
event.stopPropagation();
|
|
61
|
+
setIsTooltipVisible(false);
|
|
62
|
+
}
|
|
63
|
+
}}
|
|
39
64
|
onMouseEnter={() => {
|
|
40
65
|
if (!isTooltipMobileVisible) setIsTooltipVisible(true);
|
|
41
66
|
}}
|
|
42
|
-
onMouseLeave={
|
|
67
|
+
onMouseLeave={event => {
|
|
68
|
+
if (!event.currentTarget.contains(document.activeElement)) setIsTooltipVisible(false);
|
|
69
|
+
}}
|
|
43
70
|
onTouchStart={handleTouchStart}
|
|
44
|
-
onTouchEnd={
|
|
71
|
+
onTouchEnd={clearHold}
|
|
72
|
+
onTouchCancel={clearHold}
|
|
73
|
+
onTouchMove={() => { clearHold(); suppressClickRef.current = true; }}
|
|
74
|
+
onClickCapture={event => {
|
|
75
|
+
if (suppressClickRef.current && event.currentTarget.contains(event.target as Node)) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
suppressClickRef.current = false;
|
|
79
|
+
}
|
|
80
|
+
}}
|
|
45
81
|
>
|
|
46
82
|
{children}
|
|
47
83
|
|
|
48
84
|
{isTooltipVisible && !isTooltipMobileVisible && (
|
|
49
|
-
<MagicTooltip spell={spell} />
|
|
85
|
+
<MagicTooltip spell={spell} anchor={wrapperRef.current} />
|
|
50
86
|
)}
|
|
51
87
|
{isTooltipMobileVisible && (
|
|
52
88
|
<MobileSpellTooltip
|
|
53
89
|
closeTooltip={() => {
|
|
54
90
|
setIsTooltipMobileVisible(false);
|
|
55
|
-
console.log('close');
|
|
56
91
|
}}
|
|
57
92
|
spell={spell}
|
|
58
93
|
scale={scale}
|
|
@@ -60,4 +95,4 @@ export const SpellInfoWrapper: React.FC<ISpellInfoWrapperProps> = ({
|
|
|
60
95
|
)}
|
|
61
96
|
</div>
|
|
62
97
|
);
|
|
63
|
-
};
|
|
98
|
+
};
|
|
@@ -6,19 +6,19 @@ import { SpellInfoDisplay } from './SpellInfoDisplay';
|
|
|
6
6
|
|
|
7
7
|
export interface IMagicTooltipProps {
|
|
8
8
|
spell: ISpell;
|
|
9
|
+
anchor?: HTMLElement | null;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const offset = 20;
|
|
12
13
|
|
|
13
|
-
export const MagicTooltip: React.FC<IMagicTooltipProps> = ({ spell }) => {
|
|
14
|
+
export const MagicTooltip: React.FC<IMagicTooltipProps> = ({ spell, anchor }) => {
|
|
14
15
|
const ref = useRef<HTMLDivElement>(null);
|
|
15
16
|
|
|
16
17
|
useEffect(() => {
|
|
17
18
|
const { current } = ref;
|
|
18
19
|
|
|
19
20
|
if (current) {
|
|
20
|
-
const
|
|
21
|
-
const { clientX, clientY } = event;
|
|
21
|
+
const position = (clientX: number, clientY: number): void => {
|
|
22
22
|
|
|
23
23
|
// Adjust the position of the tooltip based on the mouse position
|
|
24
24
|
const rect = current.getBoundingClientRect();
|
|
@@ -36,10 +36,17 @@ export const MagicTooltip: React.FC<IMagicTooltipProps> = ({ spell }) => {
|
|
|
36
36
|
? clientY - tooltipHeight - offset
|
|
37
37
|
: clientY + offset;
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
const boundedX = Math.max(8, Math.min(x, window.innerWidth - tooltipWidth - 8));
|
|
40
|
+
const boundedY = Math.max(8, Math.min(y, window.innerHeight - tooltipHeight - 8));
|
|
41
|
+
current.style.transform = `translate(${boundedX}px, ${boundedY}px)`;
|
|
40
42
|
current.style.opacity = '1';
|
|
41
43
|
};
|
|
42
44
|
|
|
45
|
+
const handleMouseMove = (event: MouseEvent): void => position(event.clientX, event.clientY);
|
|
46
|
+
if (anchor) {
|
|
47
|
+
const rect = anchor.getBoundingClientRect();
|
|
48
|
+
position(rect.right, rect.top);
|
|
49
|
+
}
|
|
43
50
|
window.addEventListener('mousemove', handleMouseMove);
|
|
44
51
|
|
|
45
52
|
return () => {
|
|
@@ -48,11 +55,11 @@ export const MagicTooltip: React.FC<IMagicTooltipProps> = ({ spell }) => {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
return;
|
|
51
|
-
}, []);
|
|
58
|
+
}, [anchor]);
|
|
52
59
|
|
|
53
60
|
return (
|
|
54
61
|
<ModalPortal>
|
|
55
|
-
<Container ref={ref}>
|
|
62
|
+
<Container ref={ref} role="tooltip" id={`spell-details-${spell.key}`}>
|
|
56
63
|
<SpellInfoDisplay spell={spell} />
|
|
57
64
|
</Container>
|
|
58
65
|
</ModalPortal>
|