Skip to content

Commit ff51fcd

Browse files
committedAug 8, 2024·
feat(nx-dev): improve tab UX
1 parent 46dcee6 commit ff51fcd

File tree

4 files changed

+80
-42
lines changed

4 files changed

+80
-42
lines changed
 

‎nx-dev/ui-fence/src/lib/fence.tsx

+18-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ClipboardDocumentIcon,
55
SparklesIcon,
66
} from '@heroicons/react/24/outline';
7+
import cx from 'classnames';
78
import { JSX, ReactNode, useEffect, useState } from 'react';
89
// @ts-ignore
910
import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -31,6 +32,7 @@ function CodeWrapper(options: {
3132
title: string;
3233
path: string;
3334
language: string;
35+
isWithinTab?: boolean;
3436
children: string; // intentionally typed as such
3537
}): ({ children }: { children: ReactNode }) => JSX.Element {
3638
return ({ children }: { children: ReactNode }) =>
@@ -49,7 +51,11 @@ function CodeWrapper(options: {
4951
title={options.title}
5052
/>
5153
) : (
52-
<CodeOutput content={children} fileName={options.fileName} />
54+
<CodeOutput
55+
content={children}
56+
fileName={options.fileName}
57+
isWithinTab={options.isWithinTab}
58+
/>
5359
);
5460
}
5561

@@ -92,6 +98,7 @@ export interface FenceProps {
9298
skipRescope?: boolean;
9399
selectedLineGroup?: string;
94100
onLineGroupSelectionChange?: (selection: string) => void;
101+
isWithinTab?: boolean;
95102
}
96103

97104
export function Fence({
@@ -107,6 +114,7 @@ export function Fence({
107114
selectedLineGroup,
108115
skipRescope,
109116
onLineGroupSelectionChange,
117+
isWithinTab,
110118
}: FenceProps) {
111119
if (highlightLines) {
112120
highlightLines = processHighlightLines(highlightLines);
@@ -168,7 +176,12 @@ export function Fence({
168176
}
169177

170178
return (
171-
<div className="code-block group relative w-full">
179+
<div
180+
className={cx(
181+
'code-block group relative',
182+
isWithinTab ? '-ml-4 -mr-4 w-[calc(100%+2rem)]' : 'w-auto'
183+
)}
184+
>
172185
<div>
173186
<div className="absolute right-0 top-0 z-10 flex">
174187
{enableCopy && enableCopy === true && (
@@ -182,7 +195,7 @@ export function Fence({
182195
type="button"
183196
className={
184197
'not-prose flex border border-slate-200 bg-slate-50/50 p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:border-slate-700 dark:bg-slate-800/60' +
185-
(highlightOptions && highlightOptions[0]
198+
((highlightOptions && highlightOptions[0]) || isWithinTab
186199
? ''
187200
: ' rounded-tr-lg')
188201
}
@@ -197,7 +210,7 @@ export function Fence({
197210
)}
198211
{highlightOptions && highlightOptions[0] && (
199212
<Selector
200-
className="rounded-tr-lg"
213+
className={cx(isWithinTab ? '' : 'rounded-tr-lg')}
201214
items={highlightOptions}
202215
selected={selectedOption}
203216
onChange={highlightChange}
@@ -219,6 +232,7 @@ export function Fence({
219232
path,
220233
language,
221234
children,
235+
isWithinTab,
222236
})}
223237
/>
224238
</div>

‎nx-dev/ui-fence/src/lib/fences/code-output.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { JSX, ReactNode } from 'react';
2+
import cx from 'classnames';
23

34
export function CodeOutput({
45
content,
56
fileName,
7+
isWithinTab,
68
}: {
79
content: ReactNode;
810
fileName: string;
11+
isWithinTab?: boolean;
912
}): JSX.Element {
1013
return (
11-
<div className="hljs not-prose w-full overflow-x-auto rounded-lg border border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60">
14+
<div
15+
className={cx(
16+
'hljs not-prose w-full overflow-x-auto border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60',
17+
isWithinTab ? 'border-b border-t' : 'rounded-lg border'
18+
)}
19+
>
1220
{!!fileName && (
1321
<div className="flex border-b border-slate-200 bg-slate-50 px-4 py-2 italic text-slate-400 dark:border-slate-700 dark:bg-slate-800/80 dark:text-slate-500">
1422
{fileName}

‎nx-dev/ui-markdoc/src/lib/nodes/fence-wrapper.component.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function FenceWrapper(props: FenceProps) {
3434
};
3535

3636
return (
37-
<div className="my-8 w-full">
37+
<div className="w-full">
3838
<Fence {...modifiedProps} />
3939
</div>
4040
);
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client';
2-
// TODO@ben: refactor to use HeadlessUI tabs
32
import cx from 'classnames';
4-
import {
3+
import React, {
54
createContext,
65
ReactNode,
76
useContext,
87
useEffect,
98
useState,
9+
cloneElement,
1010
} from 'react';
1111

1212
export const TabContext = createContext('');
@@ -20,51 +20,58 @@ export function Tabs({
2020
labels: string[];
2121
children: ReactNode;
2222
}) {
23-
const [currentTab, setCurrentTab] = useState(labels[0]);
23+
const [currentTab, setCurrentTab] = useState<string>(labels[0]);
24+
2425
useEffect(() => {
2526
const handleTabSelectedEvent = () => {
2627
const selectedTab = localStorage.getItem(SELECTED_TAB_KEY);
2728
if (selectedTab && labels.includes(selectedTab)) {
2829
setCurrentTab(selectedTab);
2930
}
3031
};
32+
3133
handleTabSelectedEvent();
3234
window.addEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
3335
return () =>
3436
window.removeEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
3537
}, [labels]);
3638

39+
const handleTabClick = (label: string) => {
40+
localStorage.setItem(SELECTED_TAB_KEY, label);
41+
window.dispatchEvent(new Event(TAB_SELECTED_EVENT));
42+
setCurrentTab(label);
43+
};
44+
3745
return (
3846
<TabContext.Provider value={currentTab}>
39-
<section>
40-
<div className="not-prose ">
41-
<div className="border-b border-slate-200 dark:border-slate-800">
42-
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
43-
{labels.map((label: string) => (
44-
<button
45-
key={label}
46-
role="tab"
47-
aria-selected={label === currentTab}
48-
onClick={() => {
49-
localStorage.setItem(SELECTED_TAB_KEY, label);
50-
window.dispatchEvent(new Event(TAB_SELECTED_EVENT));
51-
setCurrentTab(label);
52-
}}
53-
className={cx(
54-
'whitespace-nowrap border-b-2 border-transparent p-2 text-sm font-medium',
55-
label === currentTab
56-
? 'border-blue-500 text-slate-800 dark:border-sky-500 dark:text-slate-300'
57-
: 'text-slate-500 hover:border-blue-500 hover:text-slate-800 dark:text-slate-400 dark:hover:border-sky-500 dark:hover:text-slate-300'
58-
)}
59-
>
60-
{label}
61-
</button>
62-
))}
63-
</nav>
64-
</div>
65-
</div>
47+
<nav className="not-prose -mb-px flex space-x-8" aria-label="Tabs">
48+
{labels.map((label, index) => (
49+
<button
50+
key={label}
51+
role="tab"
52+
aria-selected={label === currentTab}
53+
onClick={() => handleTabClick(label)}
54+
className={cx(
55+
'whitespace-nowrap border-b-2 p-2 text-sm font-medium',
56+
label === currentTab
57+
? 'border-blue-500 text-slate-800 dark:border-sky-500 dark:text-slate-300'
58+
: 'border-transparent text-slate-500 hover:border-blue-500 hover:text-slate-800 dark:text-slate-400 dark:hover:border-sky-500 dark:hover:text-slate-300'
59+
)}
60+
>
61+
{label}
62+
</button>
63+
))}
64+
</nav>
65+
<div
66+
className={cx(
67+
'border border-slate-200 pb-2 pl-4 pr-4 pt-2 dark:border-slate-700',
68+
currentTab === labels[0]
69+
? 'rounded-b-md rounded-tr-md'
70+
: 'rounded-b-md rounded-t-md'
71+
)}
72+
>
6673
{children}
67-
</section>
74+
</div>
6875
</TabContext.Provider>
6976
);
7077
}
@@ -77,14 +84,23 @@ export function Tab({
7784
children: ReactNode;
7885
}) {
7986
const currentTab = useContext(TabContext);
87+
const isActive = label === currentTab;
8088

81-
if (label !== currentTab) {
82-
return null;
83-
}
89+
const passPropsToChildren = (children: ReactNode) => {
90+
return React.Children.map(children, (child) => {
91+
if (React.isValidElement(child) && typeof child.type !== 'string') {
92+
return cloneElement(child, { isWithinTab: true });
93+
}
94+
return child;
95+
});
96+
};
8497

8598
return (
86-
<div className="prose prose-slate dark:prose-invert mt-4 max-w-none">
87-
{children}
99+
<div
100+
className="prose prose-slate dark:prose-invert mt-2 max-w-none"
101+
hidden={!isActive}
102+
>
103+
{isActive && passPropsToChildren(children)}
88104
</div>
89105
);
90106
}

0 commit comments

Comments
 (0)
Please sign in to comment.