From 14d5e2edb3cb9a8fc5b8791413fd839af90ccbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 24 Nov 2023 14:16:23 +0200 Subject: [PATCH] @thunderstore/cyberstorm: add NewTabs Add an improved version Tabs component. The old Tabs component is retained for now to avoid scope creep. Ideally eventually all of the old components will be replaced, the old implementation removed and NewTabs renamed to Tabs. The main motivation for the new implementation is an usecase from PackageDetailLayout. Dapper will initally load only content for the default tab. Additionally it will load flags defining whether other tabs have content. E.g. if the changelog tab has no content, the tab is disabled. If changelog exists, it's loaded with a separate Dapper call. Additional improvements include: - Moving away from "children as props" approach - Ease of use when developer doesn't have to provide the tab buttons and contents separately - Ease of use when the Tabs component will internally handle switching between the tabs - Probably cleaner support for Suspenses Refs TS-1979 --- .../src/components/NewTabs/Tabs.module.css | 54 +++++++ .../src/components/NewTabs/Tabs.tsx | 148 ++++++++++++++++++ packages/cyberstorm/src/index.ts | 1 + 3 files changed, 203 insertions(+) create mode 100644 packages/cyberstorm/src/components/NewTabs/Tabs.module.css create mode 100644 packages/cyberstorm/src/components/NewTabs/Tabs.tsx diff --git a/packages/cyberstorm/src/components/NewTabs/Tabs.module.css b/packages/cyberstorm/src/components/NewTabs/Tabs.module.css new file mode 100644 index 000000000..a0a2814a3 --- /dev/null +++ b/packages/cyberstorm/src/components/NewTabs/Tabs.module.css @@ -0,0 +1,54 @@ +.root { + display: flex; + flex-direction: column; + gap: var(--space--32); +} + +.buttons { + display: flex; + overflow: auto; +} + +.button { + display: flex; + flex: none; + flex-direction: row; + gap: var(--space--12); + align-items: center; + justify-content: center; + + padding: var(--space--12) var(--space--16); + border-bottom: 3px solid var(--color-purple--5); + color: var(--tab-color); + background-color: transparent; + + --tab-color: var(--color-text--default); +} + +.button.active { + border-color: var(--tab-color); + + --tab-color: var(--color-green--5); +} + +.button:disabled { + --tab-color: var(--color-border--highlight); +} + +.icon { + color: var(--color-border--highlight); +} + +.button.active .icon { + color: var(--tab-color); +} + +.button:not(.active, :disabled):hover > .icon { + color: var(--color-text--tertiary); +} + +.label { + font-weight: var(--font-weight-bold); + font-size: var(--font-size--l); + line-height: 1.3; +} diff --git a/packages/cyberstorm/src/components/NewTabs/Tabs.tsx b/packages/cyberstorm/src/components/NewTabs/Tabs.tsx new file mode 100644 index 000000000..a23395695 --- /dev/null +++ b/packages/cyberstorm/src/components/NewTabs/Tabs.tsx @@ -0,0 +1,148 @@ +"use client"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import React, { PropsWithChildren, startTransition, useState } from "react"; + +import styles from "./Tabs.module.css"; +import { Icon } from "../Icon/Icon"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { classnames } from "../../utils/utils"; + +interface Props extends PropsWithChildren { + /** + * Name prop of the Tab that should be shown by default. + * If omitted, the first Tab acts as the default Tab. + */ + defaultActive?: string; +} + +/** + * Wrapper components for Tab components. + * + * Validates the passed Tab components and handles switching between + * them. + */ +const Tabs = (props: Props) => { + const { defaultActive } = props; + + // Sanity checking the child components. + const children = React.Children.toArray(props.children); + const seenNames: string[] = []; + + children.forEach((child) => { + if (!React.isValidElement(child) || child.type !== Tab) { + throw new Error("Tabs component only allows Tab child components"); + } + if (seenNames.includes(child.props.name)) { + throw new Error(`Non-unique Tab.name "${child.props.name}" in Tabs`); + } + seenNames.push(child.props.name); + }); + + if (defaultActive && !seenNames.includes(defaultActive)) { + throw new Error( + `Tabs.defaultActive "${defaultActive}" matches no child Tab.name` + ); + } + + const [activeTab, setActiveTab] = useState(defaultActive || seenNames[0]); + + // Throw away the ease-of-use Tab components and render internal + // components instead. + const buttons: JSX.Element[] = []; + const contents: JSX.Element[] = []; + + children.forEach((child) => { + if (React.isValidElement(child)) { + buttons.push( + setActiveTab(child.props.name)} + {...child.props} + /> + ); + contents.push( + + {child.props.children} + + ); + } + }); + + return ( +
+
{buttons}
+
{contents}
+
+ ); +}; + +Tabs.displayName = "Tabs"; + +interface TabProps extends PropsWithChildren { + /** Unique identifier for the Tab in the context of Tabs */ + name: string; + /** Text shown on the Tab activator */ + label: string; + /** + * Prevents user from selecting the Tab. + * + * Note that if default Tab is disabled, the contents will be shown + * initially, but user can't reselect the Tab once they leave it. + */ + disabled?: boolean; + /** Icon shown on the Tab activator */ + icon?: IconDefinition; +} + +const Tab = (props: TabProps) => props && null; + +Tab.displayName = "Tab"; + +export default Object.assign(Tabs, { Tab: Tab }); + +interface InternalTabButtonProps { + label: string; + active: boolean; + setActive: () => void; + disabled?: boolean; + icon?: IconDefinition; +} + +/** Only for internal use by Tabs and thus not exported. */ +const InternalTabButton = (props: InternalTabButtonProps) => { + const { active, disabled = false, icon, label, setActive } = props; + + return ( + + ); +}; + +InternalTabButton.displayName = "InternalTabButton"; + +interface InternalTabContentProps extends PropsWithChildren { + active: boolean; +} + +/** Only for internal use by Tabs and thus not exported. */ +const InternalTabContent = (props: InternalTabContentProps) => { + return props.active ? <>{props.children} : <>; +}; + +InternalTabContent.displayName = "InternalTabContent"; diff --git a/packages/cyberstorm/src/index.ts b/packages/cyberstorm/src/index.ts index b5e488ef9..7231055c0 100644 --- a/packages/cyberstorm/src/index.ts +++ b/packages/cyberstorm/src/index.ts @@ -53,6 +53,7 @@ export type { MenuItemProps } from "./components/MenuItem/"; export { MetaItem, type MetaItemProps } from "./components/MetaItem/MetaItem"; export { MetaInfoItem } from "./components/MetaInfoItem/MetaInfoItem"; export { MetaInfoItemList } from "./components/MetaInfoItemList/MetaInfoItemList"; +export { default as NewTabs } from "./components/NewTabs/Tabs"; export { Switch, type SwitchProps } from "./components/Switch/Switch"; export { PackageCard } from "./components/PackageCard/PackageCard"; export {