feat(nx-dev): improve tab UX
This commit is contained in:
parent
46dcee6dd2
commit
ff51fcd2cd
@ -4,6 +4,7 @@ import {
|
|||||||
ClipboardDocumentIcon,
|
ClipboardDocumentIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
|
import cx from 'classnames';
|
||||||
import { JSX, ReactNode, useEffect, useState } from 'react';
|
import { JSX, ReactNode, useEffect, useState } from 'react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
@ -31,6 +32,7 @@ function CodeWrapper(options: {
|
|||||||
title: string;
|
title: string;
|
||||||
path: string;
|
path: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
isWithinTab?: boolean;
|
||||||
children: string; // intentionally typed as such
|
children: string; // intentionally typed as such
|
||||||
}): ({ children }: { children: ReactNode }) => JSX.Element {
|
}): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||||
return ({ children }: { children: ReactNode }) =>
|
return ({ children }: { children: ReactNode }) =>
|
||||||
@ -49,7 +51,11 @@ function CodeWrapper(options: {
|
|||||||
title={options.title}
|
title={options.title}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CodeOutput content={children} fileName={options.fileName} />
|
<CodeOutput
|
||||||
|
content={children}
|
||||||
|
fileName={options.fileName}
|
||||||
|
isWithinTab={options.isWithinTab}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +98,7 @@ export interface FenceProps {
|
|||||||
skipRescope?: boolean;
|
skipRescope?: boolean;
|
||||||
selectedLineGroup?: string;
|
selectedLineGroup?: string;
|
||||||
onLineGroupSelectionChange?: (selection: string) => void;
|
onLineGroupSelectionChange?: (selection: string) => void;
|
||||||
|
isWithinTab?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Fence({
|
export function Fence({
|
||||||
@ -107,6 +114,7 @@ export function Fence({
|
|||||||
selectedLineGroup,
|
selectedLineGroup,
|
||||||
skipRescope,
|
skipRescope,
|
||||||
onLineGroupSelectionChange,
|
onLineGroupSelectionChange,
|
||||||
|
isWithinTab,
|
||||||
}: FenceProps) {
|
}: FenceProps) {
|
||||||
if (highlightLines) {
|
if (highlightLines) {
|
||||||
highlightLines = processHighlightLines(highlightLines);
|
highlightLines = processHighlightLines(highlightLines);
|
||||||
@ -168,7 +176,12 @@ export function Fence({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="code-block group relative w-full">
|
<div
|
||||||
|
className={cx(
|
||||||
|
'code-block group relative',
|
||||||
|
isWithinTab ? '-ml-4 -mr-4 w-[calc(100%+2rem)]' : 'w-auto'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="absolute right-0 top-0 z-10 flex">
|
<div className="absolute right-0 top-0 z-10 flex">
|
||||||
{enableCopy && enableCopy === true && (
|
{enableCopy && enableCopy === true && (
|
||||||
@ -182,7 +195,7 @@ export function Fence({
|
|||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
'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' +
|
'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' +
|
||||||
(highlightOptions && highlightOptions[0]
|
((highlightOptions && highlightOptions[0]) || isWithinTab
|
||||||
? ''
|
? ''
|
||||||
: ' rounded-tr-lg')
|
: ' rounded-tr-lg')
|
||||||
}
|
}
|
||||||
@ -197,7 +210,7 @@ export function Fence({
|
|||||||
)}
|
)}
|
||||||
{highlightOptions && highlightOptions[0] && (
|
{highlightOptions && highlightOptions[0] && (
|
||||||
<Selector
|
<Selector
|
||||||
className="rounded-tr-lg"
|
className={cx(isWithinTab ? '' : 'rounded-tr-lg')}
|
||||||
items={highlightOptions}
|
items={highlightOptions}
|
||||||
selected={selectedOption}
|
selected={selectedOption}
|
||||||
onChange={highlightChange}
|
onChange={highlightChange}
|
||||||
@ -219,6 +232,7 @@ export function Fence({
|
|||||||
path,
|
path,
|
||||||
language,
|
language,
|
||||||
children,
|
children,
|
||||||
|
isWithinTab,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
import { JSX, ReactNode } from 'react';
|
import { JSX, ReactNode } from 'react';
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
export function CodeOutput({
|
export function CodeOutput({
|
||||||
content,
|
content,
|
||||||
fileName,
|
fileName,
|
||||||
|
isWithinTab,
|
||||||
}: {
|
}: {
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
isWithinTab?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<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">
|
<div
|
||||||
|
className={cx(
|
||||||
|
'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',
|
||||||
|
isWithinTab ? 'border-b border-t' : 'rounded-lg border'
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!!fileName && (
|
{!!fileName && (
|
||||||
<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">
|
<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">
|
||||||
{fileName}
|
{fileName}
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export function FenceWrapper(props: FenceProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-8 w-full">
|
<div className="w-full">
|
||||||
<Fence {...modifiedProps} />
|
<Fence {...modifiedProps} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
// TODO@ben: refactor to use HeadlessUI tabs
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
|
cloneElement,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
export const TabContext = createContext('');
|
export const TabContext = createContext('');
|
||||||
@ -20,7 +20,8 @@ export function Tabs({
|
|||||||
labels: string[];
|
labels: string[];
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [currentTab, setCurrentTab] = useState(labels[0]);
|
const [currentTab, setCurrentTab] = useState<string>(labels[0]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleTabSelectedEvent = () => {
|
const handleTabSelectedEvent = () => {
|
||||||
const selectedTab = localStorage.getItem(SELECTED_TAB_KEY);
|
const selectedTab = localStorage.getItem(SELECTED_TAB_KEY);
|
||||||
@ -28,43 +29,49 @@ export function Tabs({
|
|||||||
setCurrentTab(selectedTab);
|
setCurrentTab(selectedTab);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleTabSelectedEvent();
|
handleTabSelectedEvent();
|
||||||
window.addEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
|
window.addEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
|
||||||
return () =>
|
return () =>
|
||||||
window.removeEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
|
window.removeEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
|
||||||
}, [labels]);
|
}, [labels]);
|
||||||
|
|
||||||
|
const handleTabClick = (label: string) => {
|
||||||
|
localStorage.setItem(SELECTED_TAB_KEY, label);
|
||||||
|
window.dispatchEvent(new Event(TAB_SELECTED_EVENT));
|
||||||
|
setCurrentTab(label);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabContext.Provider value={currentTab}>
|
<TabContext.Provider value={currentTab}>
|
||||||
<section>
|
<nav className="not-prose -mb-px flex space-x-8" aria-label="Tabs">
|
||||||
<div className="not-prose ">
|
{labels.map((label, index) => (
|
||||||
<div className="border-b border-slate-200 dark:border-slate-800">
|
<button
|
||||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
key={label}
|
||||||
{labels.map((label: string) => (
|
role="tab"
|
||||||
<button
|
aria-selected={label === currentTab}
|
||||||
key={label}
|
onClick={() => handleTabClick(label)}
|
||||||
role="tab"
|
className={cx(
|
||||||
aria-selected={label === currentTab}
|
'whitespace-nowrap border-b-2 p-2 text-sm font-medium',
|
||||||
onClick={() => {
|
label === currentTab
|
||||||
localStorage.setItem(SELECTED_TAB_KEY, label);
|
? 'border-blue-500 text-slate-800 dark:border-sky-500 dark:text-slate-300'
|
||||||
window.dispatchEvent(new Event(TAB_SELECTED_EVENT));
|
: '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'
|
||||||
setCurrentTab(label);
|
)}
|
||||||
}}
|
>
|
||||||
className={cx(
|
{label}
|
||||||
'whitespace-nowrap border-b-2 border-transparent p-2 text-sm font-medium',
|
</button>
|
||||||
label === currentTab
|
))}
|
||||||
? 'border-blue-500 text-slate-800 dark:border-sky-500 dark:text-slate-300'
|
</nav>
|
||||||
: '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'
|
<div
|
||||||
)}
|
className={cx(
|
||||||
>
|
'border border-slate-200 pb-2 pl-4 pr-4 pt-2 dark:border-slate-700',
|
||||||
{label}
|
currentTab === labels[0]
|
||||||
</button>
|
? 'rounded-b-md rounded-tr-md'
|
||||||
))}
|
: 'rounded-b-md rounded-t-md'
|
||||||
</nav>
|
)}
|
||||||
</div>
|
>
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</div>
|
||||||
</TabContext.Provider>
|
</TabContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -77,14 +84,23 @@ export function Tab({
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const currentTab = useContext(TabContext);
|
const currentTab = useContext(TabContext);
|
||||||
|
const isActive = label === currentTab;
|
||||||
|
|
||||||
if (label !== currentTab) {
|
const passPropsToChildren = (children: ReactNode) => {
|
||||||
return null;
|
return React.Children.map(children, (child) => {
|
||||||
}
|
if (React.isValidElement(child) && typeof child.type !== 'string') {
|
||||||
|
return cloneElement(child, { isWithinTab: true });
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="prose prose-slate dark:prose-invert mt-4 max-w-none">
|
<div
|
||||||
{children}
|
className="prose prose-slate dark:prose-invert mt-2 max-w-none"
|
||||||
|
hidden={!isActive}
|
||||||
|
>
|
||||||
|
{isActive && passPropsToChildren(children)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user