Skip to content

Commit 7861b2d

Browse files
authored
Merge pull request #15 from ueberdosis/web/patch-2
Refactor Link Handling & UI Enhancements: Split Node Buttons, Rename Highlight, Fix Link Popover
2 parents b9c4f5c + cdfcf2a commit 7861b2d

File tree

21 files changed

+521
-275
lines changed

21 files changed

+521
-275
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ The [components](https://tiptap.dev/docs/ui-components/components/overview) avai
4343

4444
#### UI Components
4545

46+
- Blockquote button
47+
- Code block button
48+
- Color highlight button / popover
4649
- Heading button / dropdown
47-
- Highlight popover
4850
- Image upload button
4951
- Link popover
5052
- List button / dropdown
5153
- Mark button
52-
- Node button
5354
- Text align button
5455
- Undo redo button
5556

apps/web/src/components/tiptap-templates/simple/data/content.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
{
3434
"type": "highlight",
3535
"attrs": {
36-
"color": "var(--tt-highlight-yellow)"
36+
"color": "var(--tt-color-highlight-yellow)"
3737
}
3838
}
3939
],
@@ -209,7 +209,7 @@
209209
{
210210
"type": "highlight",
211211
"attrs": {
212-
"color": "var(--tt-highlight-blue)"
212+
"color": "var(--tt-color-highlight-blue)"
213213
}
214214
}
215215
],

apps/web/src/components/tiptap-templates/simple/simple-editor.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ import "@/components/tiptap-node/paragraph-node/paragraph-node.scss"
3838
import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu"
3939
import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button"
4040
import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu"
41-
import { NodeButton } from "@/components/tiptap-ui/node-button"
41+
import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button"
42+
import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button"
4243
import {
43-
HighlightPopover,
44-
HighlightContent,
45-
HighlighterButton,
46-
} from "@/components/tiptap-ui/highlight-popover"
44+
ColorHighlightPopover,
45+
ColorHighlightPopoverContent,
46+
ColorHighlightPopoverButton,
47+
} from "@/components/tiptap-ui/color-highlight-popover"
4748
import {
4849
LinkPopover,
4950
LinkContent,
@@ -97,8 +98,8 @@ const MainToolbarContent = ({
9798
<ToolbarGroup>
9899
<HeadingDropdownMenu levels={[1, 2, 3, 4]} />
99100
<ListDropdownMenu types={["bulletList", "orderedList", "taskList"]} />
100-
<NodeButton type="codeBlock" />
101-
<NodeButton type="blockquote" />
101+
<BlockquoteButton />
102+
<CodeBlockButton />
102103
</ToolbarGroup>
103104

104105
<ToolbarSeparator />
@@ -110,9 +111,9 @@ const MainToolbarContent = ({
110111
<MarkButton type="code" />
111112
<MarkButton type="underline" />
112113
{!isMobile ? (
113-
<HighlightPopover />
114+
<ColorHighlightPopover />
114115
) : (
115-
<HighlighterButton onClick={onHighlighterClick} />
116+
<ColorHighlightPopoverButton onClick={onHighlighterClick} />
116117
)}
117118
{!isMobile ? <LinkPopover /> : <LinkButton onClick={onLinkClick} />}
118119
</ToolbarGroup>
@@ -171,7 +172,11 @@ const MobileToolbarContent = ({
171172

172173
<ToolbarSeparator />
173174

174-
{type === "highlighter" ? <HighlightContent /> : <LinkContent />}
175+
{type === "highlighter" ? (
176+
<ColorHighlightPopoverContent />
177+
) : (
178+
<LinkContent />
179+
)}
175180
</>
176181
)
177182

apps/web/src/components/tiptap-ui-primitive/button/button.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
outline: none;
2424
}
2525

26-
&[data-highlighted="true"] {
26+
&[data-highlighted="true"],
27+
&[data-focus-visible="true"] {
2728
background-color: var(--tt-button-hover-bg-color);
2829
color: var(--tt-button-hover-text-color);
2930
// outline: 2px solid var(--tt-button-active-icon-color);

apps/web/src/components/tiptap-ui-primitive/popover/popover.tsx

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ type PopoverContextValue = ReturnType<typeof usePopover> & {
2222
setDescriptionId: (id: string | undefined) => void
2323
updatePosition: (
2424
side: "top" | "right" | "bottom" | "left",
25-
align: "start" | "center" | "end"
25+
align: "start" | "center" | "end",
26+
sideOffset?: number,
27+
alignOffset?: number
2628
) => void
2729
}
2830

@@ -33,6 +35,8 @@ interface PopoverOptions {
3335
onOpenChange?: (open: boolean) => void
3436
side?: "top" | "right" | "bottom" | "left"
3537
align?: "start" | "center" | "end"
38+
sideOffset?: number
39+
alignOffset?: number
3640
}
3741

3842
interface PopoverProps extends PopoverOptions {
@@ -56,29 +60,35 @@ function usePopover({
5660
onOpenChange: setControlledOpen,
5761
side = "bottom",
5862
align = "center",
63+
sideOffset = 4,
64+
alignOffset = 0,
5965
}: PopoverOptions = {}) {
6066
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen)
6167
const [labelId, setLabelId] = React.useState<string>()
6268
const [descriptionId, setDescriptionId] = React.useState<string>()
6369
const [currentPlacement, setCurrentPlacement] = React.useState<Placement>(
6470
`${side}-${align}` as Placement
6571
)
72+
const [offsets, setOffsets] = React.useState({ sideOffset, alignOffset })
6673

6774
const open = controlledOpen ?? uncontrolledOpen
6875
const setOpen = setControlledOpen ?? setUncontrolledOpen
6976

7077
const middleware = React.useMemo(
7178
() => [
72-
offset(4),
79+
offset({
80+
mainAxis: offsets.sideOffset,
81+
crossAxis: offsets.alignOffset,
82+
}),
7383
flip({
7484
fallbackAxisSideDirection: "end",
7585
crossAxis: false,
7686
}),
7787
shift({
78-
limiter: limitShift({ offset: 4 }),
88+
limiter: limitShift({ offset: offsets.sideOffset }),
7989
}),
8090
],
81-
[]
91+
[offsets.sideOffset, offsets.alignOffset]
8292
)
8393

8494
const floating = useFloating({
@@ -98,11 +108,19 @@ function usePopover({
98108
const updatePosition = React.useCallback(
99109
(
100110
newSide: "top" | "right" | "bottom" | "left",
101-
newAlign: "start" | "center" | "end"
111+
newAlign: "start" | "center" | "end",
112+
newSideOffset?: number,
113+
newAlignOffset?: number
102114
) => {
103115
setCurrentPlacement(`${newSide}-${newAlign}` as Placement)
116+
if (newSideOffset !== undefined || newAlignOffset !== undefined) {
117+
setOffsets({
118+
sideOffset: newSideOffset ?? offsets.sideOffset,
119+
alignOffset: newAlignOffset ?? offsets.alignOffset,
120+
})
121+
}
104122
},
105-
[]
123+
[offsets.sideOffset, offsets.alignOffset]
106124
)
107125

108126
return React.useMemo(
@@ -181,8 +199,11 @@ const PopoverTrigger = React.forwardRef<HTMLElement, TriggerElementProps>(
181199
interface PopoverContentProps extends React.HTMLProps<HTMLDivElement> {
182200
side?: "top" | "right" | "bottom" | "left"
183201
align?: "start" | "center" | "end"
202+
sideOffset?: number
203+
alignOffset?: number
184204
portal?: boolean
185205
portalProps?: Omit<React.ComponentProps<typeof FloatingPortal>, "children">
206+
asChild?: boolean
186207
}
187208

188209
const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
@@ -191,50 +212,69 @@ const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
191212
className,
192213
side = "bottom",
193214
align = "center",
215+
sideOffset,
216+
alignOffset,
194217
style,
195218
portal = true,
196219
portalProps = {},
220+
asChild = false,
221+
children,
197222
...props
198223
},
199224
propRef
200225
) {
201226
const context = usePopoverContext()
202-
const ref = useMergeRefs([context.refs.setFloating, propRef])
227+
const childrenRef = React.isValidElement(children)
228+
? parseInt(React.version, 10) >= 19
229+
? (children.props as any).ref
230+
: (children as any).ref
231+
: undefined
232+
const ref = useMergeRefs([context.refs.setFloating, propRef, childrenRef])
203233

204234
React.useEffect(() => {
205-
context.updatePosition(side, align)
206-
}, [context, side, align])
235+
context.updatePosition(side, align, sideOffset, alignOffset)
236+
}, [context, side, align, sideOffset, alignOffset])
207237

208238
if (!context.context.open) return null
209239

210-
const content = (
240+
const contentProps = {
241+
ref,
242+
style: {
243+
position: context.strategy,
244+
top: context.y ?? 0,
245+
left: context.x ?? 0,
246+
...style,
247+
},
248+
"aria-labelledby": context.labelId,
249+
"aria-describedby": context.descriptionId,
250+
className: `tiptap-popover ${className || ""}`,
251+
"data-side": side,
252+
"data-align": align,
253+
"data-state": context.context.open ? "open" : "closed",
254+
...context.getFloatingProps(props),
255+
}
256+
257+
const content =
258+
asChild && React.isValidElement(children) ? (
259+
React.cloneElement(children, {
260+
...contentProps,
261+
...(children.props as any),
262+
})
263+
) : (
264+
<div {...contentProps}>{children}</div>
265+
)
266+
267+
const wrappedContent = (
211268
<FloatingFocusManager context={context.context} modal={context.modal}>
212-
<div
213-
ref={ref}
214-
style={{
215-
position: context.strategy,
216-
top: context.y ?? 0,
217-
left: context.x ?? 0,
218-
...style,
219-
}}
220-
aria-labelledby={context.labelId}
221-
aria-describedby={context.descriptionId}
222-
className={`tiptap-popover ${className || ""}`}
223-
data-side={side}
224-
data-align={align}
225-
data-state={context.context.open ? "open" : "closed"}
226-
{...context.getFloatingProps(props)}
227-
>
228-
{props.children}
229-
</div>
269+
{content}
230270
</FloatingFocusManager>
231271
)
232272

233273
if (portal) {
234-
return <FloatingPortal {...portalProps}>{content}</FloatingPortal>
274+
return <FloatingPortal {...portalProps}>{wrappedContent}</FloatingPortal>
235275
}
236276

237-
return content
277+
return wrappedContent
238278
}
239279
)
240280

apps/web/src/components/tiptap-ui-primitive/toolbar/toolbar.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,41 @@ const useToolbarKeyboardNav = (
114114
}
115115
}
116116

117+
const handleFocus = (e: FocusEvent) => {
118+
const target = e.target as HTMLElement
119+
if (toolbar.contains(target)) {
120+
target.setAttribute("data-focus-visible", "true")
121+
}
122+
}
123+
124+
const handleBlur = (e: FocusEvent) => {
125+
const target = e.target as HTMLElement
126+
if (toolbar.contains(target)) {
127+
target.removeAttribute("data-focus-visible")
128+
}
129+
}
130+
117131
toolbar.addEventListener("keydown", handleKeyDown)
118-
return () => toolbar.removeEventListener("keydown", handleKeyDown)
132+
toolbar.addEventListener("focus", handleFocus, true)
133+
toolbar.addEventListener("blur", handleBlur, true)
134+
135+
const focusableElements = getFocusableElements()
136+
focusableElements.forEach((element) => {
137+
element.addEventListener("focus", handleFocus)
138+
element.addEventListener("blur", handleBlur)
139+
})
140+
141+
return () => {
142+
toolbar.removeEventListener("keydown", handleKeyDown)
143+
toolbar.removeEventListener("focus", handleFocus, true)
144+
toolbar.removeEventListener("blur", handleBlur, true)
145+
146+
const focusableElements = getFocusableElements()
147+
focusableElements.forEach((element) => {
148+
element.removeEventListener("focus", handleFocus)
149+
element.removeEventListener("blur", handleBlur)
150+
})
151+
}
119152
}, [toolbarRef])
120153
}
121154

0 commit comments

Comments
 (0)