模仿Mantine Tabs

阅读本文需要的前提知识。

如果你具备React的基本知识和TypeScript的基本知识,将能够更好地理解并从中受益。
此外,熟悉React组件结构和props概念将使你更轻松地理解这篇文章。

曼泰牌标签

    • mantine Tabs

 

    src

Mantine tabs 有什么特别之处?

大多数的UI组件仅由一个类构成。

 

一方面,mantine tabs 设想了由多个类组成的结构。Tabs 在内部还具有提供者,并且数据在 Tabs 组内进行完整构造。

Tabs という塊の親

Tabs の上部に位置する navigation

List の各アイテム

各 Tab に紐づくコンテンツ

<Tabs defaultValue="gallery">
  <Tabs.List>
    <Tabs.Tab value="gallery" leftSection={<IconPhoto style={iconStyle} />}>
      Gallery
    </Tabs.Tab>
    <Tabs.Tab value="messages" leftSection={<IconMessageCircle style={iconStyle} />}>
      Messages
    </Tabs.Tab>
    <Tabs.Tab value="settings" leftSection={<IconSettings style={iconStyle} />}>
      Settings
    </Tabs.Tab>
  </Tabs.List>

  <Tabs.Panel value="gallery">
    Gallery tab content
  </Tabs.Panel>

  <Tabs.Panel value="messages">
    Messages tab content
  </Tabs.Panel>

  <Tabs.Panel value="settings">
    Settings tab content
  </Tabs.Panel>
</Tabs>
ezgif.com-video-to-gif (2).gif

引用
https://mantine.dev/core/tabs/

尝试创建一个类似 Tabs.* 的组件。

思维方式

    如果我们使用 React.FC 来定义 Tabs,那么如果我们想要使 Tabs.Tab 成为可能,我们可以先继承 React.FC,不是吗?

实施

import React, {ReactElement, ReactNode, forwardRef} from 'react';

interface TabsProps {
  children: ReactNode | ReactNode[];
}

// POINT: Tabs extends React.FC
interface TabsComponent extends React.FC<TabsProps> {
  Tab: typeof Tab;
}

interface TabProps {
  children: ReactNode | ReactNode[];
}

/** -------- Components -------- */
const Tab: React.FC<TabProps> = props => {
  return (
    <div style={{padding: 8, margin: 8, border: '1px solid black'}}>
      {props.children}
    </div>
  );
};

const Tabs: TabsComponent = props => {
  return <div style={{display: 'flex'}}>{props.children}</div>;
};

Tabs.Tab = Tab;
export default Tabs;

<Tabs>
    <Tabs.Tab>1</Tabs.Tab>
    <Tabs.Tab>2</Tabs.Tab>
    <Tabs.Tab>3</Tabs.Tab>
</Tabs>

这样的事情竟然发生了… 笑

image.png

尝试创建TabsProvider

首先,关于是否需要提供者的问题,

Tabs の children が必ず Tabs.Tab になる

如果会始终遵守这个规则,我认为是不需要的。
原因是,从Tabs传递到Tabs.Tab将需要特定的props。
例如,假设Tabs.Tab需要isActive: boolean。

<div>
    {React.Children.map(props.children, (child, idx) => {
        return React.cloneElement(child, { isActive: /** TODO */});
    })}
</div>

然而,事实上

<Tabs>
    <p>I shoudn't be here!</p>
</Tabs>

这种实现方式太强大了。这个组件没有isActive这个props。结果会导致typescript错误。

所以 (suo yi)

    children の props バケツリレーで値を渡す

说到这一点

    children の に provider で値を渡す

避免它。

实施

-import React, {ReactElement, ReactNode, forwardRef} from 'react';
+import React, {
+  ReactNode,
+  createContext,
+  useContext,
+  useState,
+} from 'react';
 
 interface TabsProps {
   children: ReactNode | ReactNode[];
+  value: string;
 }
 
 // POINT: Tabs extends React.FC
 
 interface TabProps {
   children: ReactNode | ReactNode[];
+  value: string;
 }
 
+/** Provider */
+
+const TapsProvider = createContext({
+  value: '',
+  onClickTab: (value: string) => {},
+});
+
 /** -------- Components -------- */
 const Tab: React.FC<TabProps> = props => {
+  const {value, onClickTab} = useContext(TapsProvider);
+  const isActive = value === props.value;
+
+  const handleClick = () => {
+    onClickTab(props.value);
+  };
   return (
-    <div style={{padding: 8, margin: 8, border: '1px solid black'}}>
+    <div
+      onClick={handleClick}
+      style={{
+        padding: 8,
+        margin: 8,
+        border: '1px solid black',
+        backgroundColor: isActive ? 'salmon' : 'initial',
+      }}
+    >
       {props.children}
     </div>
   );
 };
 
 const Tabs: TabsComponent = props => {
-  return <div style={{display: 'flex'}}>{props.children}</div>;
+  const [value, setValue] = useState(props.value);
+  return (
+    <TapsProvider.Provider value={{value, onClick: setValue}}>
+      <div style={{display: 'flex'}}>{props.children}</div>
+    </TapsProvider.Provider>
+  );
 };
 
 Tabs.Tab = Tab;

<Tabs value="1">
    <Tabs.Tab value="1">1</Tabs.Tab>
    <Tabs.Tab value="2">2</Tabs.Tab>
    <Tabs.Tab value="3">3</Tabs.Tab>
</Tabs>
ezgif.com-video-to-gif.gif

追加各种要素后变成最终这样

Tabs.List 追加

Tabs.Panel 追加

import React, {ReactNode, createContext, useContext, useState} from 'react';

interface TabsProps {
  children: ReactNode | ReactNode[];
  defaultValue: string;
}

// POINT: Tabs extends React.FC
interface TabsComponent extends React.FC<TabsProps> {
  Tab: typeof Tab;
  List: typeof List;
  Panel: typeof Panel;
}

interface ListProps {
  children: ReactNode | ReactNode[];
  orientation?: 'horizontal' | 'vertical';
}
interface TabProps {
  children: ReactNode | ReactNode[];
  value: string;
}

interface PanelProps {
  children: ReactNode | ReactNode[];
  value: string;
}

/** Provider */

const TapsProvider = createContext({
  value: '',
  onClick: (value: string) => {},
});

/** -------- Components -------- */

const List: React.FC<ListProps> = ({orientation = 'horizontal', ...props}) => {
  return (
    <div
      style={{
        display: 'flex',
        flexDirection: orientation == 'horizontal' ? 'row' : 'column',
      }}
    >
      {props.children}
    </div>
  );
};

const Tab: React.FC<TabProps> = props => {
  const {value, onClick} = useContext(TapsProvider);
  const isActive = value === props.value;

  const handleClick = () => {
    onClick(props.value);
  };
  return (
    <div
      onClick={handleClick}
      style={{
        padding: 8,
        margin: 8,
        border: '1px solid black',
        backgroundColor: isActive ? 'salmon' : 'initial',
      }}
    >
      {props.children}
    </div>
  );
};

const Panel: React.FC<PanelProps> = props => {
  const {value} = useContext(TapsProvider);
  const isActive = value === props.value;
  return (
    <div style={{display: isActive ? 'initial' : 'none'}}>{props.children}</div>
  );
};

const Tabs: TabsComponent = props => {
  const [value, setValue] = useState(props.defaultValue);
  return (
    <TapsProvider.Provider value={{value, onClick: setValue}}>
      {props.children}
    </TapsProvider.Provider>
  );
};

Tabs.Tab = Tab;
Tabs.List = List;
Tabs.Panel = Panel;
export default Tabs;
<Tabs defaultValue="1">
    <Tabs.List orientation="horizontal">
        <Tabs.Tab value="1">1</Tabs.Tab>
        <Tabs.Tab value="2">2</Tabs.Tab>
        <Tabs.Tab value="3">3</Tabs.Tab>
    </Tabs.List>
    
    <Tabs.Panel value="1">contents of 1</Tabs.Panel>
    <Tabs.Panel value="2">contents of 2</Tabs.Panel>
    <Tabs.Panel value="3">contents of 3</Tabs.Panel>
</Tabs>
ezgif.com-video-to-gif (1).gif

最后

这次我们直截了当地继承了 React.FC,但 mantine 将此过程进行了泛化,并定义了所谓的工厂。

我对这个地方的完全理解还为时尚早。

请将以下内容在中文中进行同义转述,仅提供一种选项:

https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/core/factory/factory.ts

替代版本:请访问此链接 https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/core/factory/factory.ts 来获取所需的内容。

这次的重点有两个,对吧。

    1. 我认为这不是最理想的方式,但通过扩展React.FC来向函数组件添加函数组件。

通过使用Provider来封装Tabs组件。

广告
将在 10 秒后关闭
bannerAds