Skyroc Native UI

Sidebar

竖排侧边导航,常与右侧内容区配合

侧边栏(Sidebar)是竖排的分类导航:激活项高亮并在左侧显示一条指示条。它是 TreeSelect 左栏的实现,也可以单独使用。

import { Sidebar } from '@skyroc/native-ui';

基础用法

items 声明分类,每项由 key / title 组成。onIndexChange 抛出当前下标,第二个参数直接给出该项配置。

SidebarBasic.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const BASIC_ITEMS = ['推荐', '手机数码', '家用电器', '男装', '女装', '生鲜'].map(title => ({ key: title, title }));

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarBasic = () => {
  const [basicIndex, setBasicIndex] = useState(0);

  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="self-stretch"
          items={BASIC_ITEMS}
          onIndexChange={setBasicIndex}
        />
        <Panel
          description="指示器落在激活项的垂直中心,切换时做位移动画"
          title={BASIC_ITEMS[basicIndex].title}
        />
      </View>
    </View>
  );
};

export { SidebarBasic };

根节点默认 self-start(宽度由最长标题撑开)。放进定高的行容器时请自行给宽高,例如 className="w-24 self-stretch"

何时使用

  • 分类多、需要竖排展示的两栏布局(左分类、右内容)。
  • 横向的分段切换用 Tabs;两级联动的选择用 TreeSelect

不等高列表

标题换行导致各项高度不一时,指示器仍然逐项对齐 —— 每项各自 onLayout 上报布局,而不是按首项高度推算;指示器自身的高度也是实测的,因此用 classNames.indicator 改高度不会让它错位。

SidebarUnevenItems.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

/** 项高故意不一致:多行标题、单行标题混排,用来验证指示器逐项测量而不是按首项高度推算 */
const UNEVEN_ITEMS = ['热销', '家庭清洁 / 纸品', '个护', '医药健康与营养品', '母婴'].map(title => ({
  key: title,
  title
}));

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarUnevenItems = () => {
  const [unevenIndex, setUnevenIndex] = useState(3);

  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="w-24 self-stretch"
          defaultActiveIndex={3}
          items={UNEVEN_ITEMS}
          onIndexChange={setUnevenIndex}
        />
        <Panel
          description="试着点最后一项再点第二项,指示器不会越对越偏"
          title={UNEVEN_ITEMS[unevenIndex].title}
        />
      </View>
    </View>
  );
};

export { SidebarUnevenItems };

角标

badge 传内容、dot 传小红点,内部用 Badge 包裹标题。包裹层是 items-center 收缩到文字宽度的,角标因此贴着标题而不是飞到整项右边缘。

SidebarBadge.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import type { SidebarItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const BADGE_ITEMS: SidebarItem[] = [
  { key: 'all', title: '全部' },
  { badge: 3, key: 'pending', title: '待付款' },
  { key: 'shipping', title: '待收货' },
  { dot: true, key: 'review', title: '待评价' },
  { badge: 128, key: 'refund', title: '退款' }
];

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarBadge = () => {
  const [badgeIndex, setBadgeIndex] = useState(1);

  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="self-stretch"
          defaultActiveIndex={1}
          items={BADGE_ITEMS}
          onIndexChange={setBadgeIndex}
        />
        <Panel
          description="badge 传数字、dot 传小红点,角标贴着标题而不是飞到整项右边缘"
          title={BADGE_ITEMS[badgeIndex].title as string}
        />
      </View>
    </View>
  );
};

export { SidebarBadge };

自定义标题

title 接受任意节点,可以组合主副文案。字符串标题会自动补上 accessibilityLabel

SidebarCustomTitle.tsx
Loading…
import type { SidebarItem } from '@skyroc/native-ui';
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const CUSTOM_TITLE_ITEMS: SidebarItem[] = [
  {
    key: 'new',
    title: (
      <View className="items-center">
        <Text className="text-sm font-medium">新品</Text>
        <Text className="text-xs text-primary">NEW</Text>
      </View>
    )
  },
  {
    key: 'sale',
    title: (
      <View className="items-center">
        <Text className="text-sm font-medium">促销</Text>
        <Text className="text-xs text-destructive">SALE</Text>
      </View>
    )
  },
  { key: 'all', title: '全部' }
];

const SidebarCustomTitle = () => {
  const [activeKey, setActiveKey] = useState(CUSTOM_TITLE_ITEMS[0].key);

  function handleIndexChange(_index: number, item: SidebarItem) {
    setActiveKey(item.key);
  }

  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="self-stretch"
          items={CUSTOM_TITLE_ITEMS}
          onIndexChange={handleIndexChange}
        />
        <View className="flex-1 items-center justify-center gap-2 p-4">
          <Text className="text-base font-semibold">当前 key:{activeKey}</Text>
          <Text className="text-center text-sm text-muted-foreground">title 可直接传入自定义节点</Text>
        </View>
      </View>
    </View>
  );
};

export { SidebarCustomTitle };

禁用项

disabled 让该项降到 50% 不透明度、文字转灰且不响应点击。

SidebarDisabled.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import type { SidebarItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DISABLED_ITEMS: SidebarItem[] = [
  { key: 'draft', title: '草稿' },
  { disabled: true, key: 'reviewing', title: '审核中' },
  { key: 'published', title: '已发布' },
  { disabled: true, key: 'archived', title: '已下架' }
];

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarDisabled = () => {
  const [disabledIndex, setDisabledIndex] = useState(0);

  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="self-stretch"
          items={DISABLED_ITEMS}
          onIndexChange={setDisabledIndex}
        />
        <Panel
          description="禁用项整体降透明度且不响应点击"
          title={DISABLED_ITEMS[disabledIndex].title as string}
        />
      </View>
    </View>
  );
};

export { SidebarDisabled };

受控

activeIndex + onIndexChange 接管激活项。回调的第二个参数是该项配置,可以按 key 持久化选中态,而不是存一个会串位的下标。

SidebarControlled.tsx
Loading…
import { Button, Sidebar, Text } from '@skyroc/native-ui';
import type { SidebarItem } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const CONTROLLED_ITEMS = ['基本信息', '收货地址', '支付方式', '发票信息'].map(title => ({ key: title, title }));

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarControlled = () => {
  const [step, setStep] = useState(0);
  const [stepKey, setStepKey] = useState(CONTROLLED_ITEMS[0].key);

  function changeStep(offset: number) {
    const nextStep = Math.min(CONTROLLED_ITEMS.length - 1, Math.max(0, step + offset));
    setStep(nextStep);
    setStepKey(CONTROLLED_ITEMS[nextStep].key);
  }

  function handleIndexChange(index: number, item: SidebarItem) {
    setStep(index);
    setStepKey(item.key);
  }

  return (
    <View className="bg-background p-4">
      <View className="mb-4 h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          activeIndex={step}
          className="self-stretch"
          items={CONTROLLED_ITEMS}
          onIndexChange={handleIndexChange}
        />
        <Panel
          description="激活索引完全由外部 state 决定"
          title={CONTROLLED_ITEMS[step].title}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          color="secondary"
          variant="outline"
          onPress={() => changeStep(-1)}
        >
          上一项
        </Button>
        <Button
          color="primary"
          variant="tonal"
          onPress={() => changeStep(1)}
        >
          下一项
        </Button>
        <Text className="text-sm text-muted-foreground">
          index:{step} / key:{stepKey}
        </Text>
      </View>
    </View>
  );
};

export { SidebarControlled };

activeIndex位置而非身份,items 增删或重排后需要调用方自行校正。

可滚动

scrollable(默认 true)时根节点是 ScrollView,项数超出容器高度可以纵向滚动,指示器跟着内容一起滚。

SidebarScrollable.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const SCROLL_ITEMS = Array.from({ length: 20 }, (_, index) => `分类 ${String(index + 1).padStart(2, '0')}`).map(
  title => ({ key: title, title })
);

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarScrollable = () => {
  const [scrollIndex, setScrollIndex] = useState(11);

  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="self-stretch"
          defaultActiveIndex={11}
          items={SCROLL_ITEMS}
          onIndexChange={setScrollIndex}
        />
        <Panel
          description="默认激活第 12 项,向下滚动即可看到指示器"
          title={SCROLL_ITEMS[scrollIndex].title}
        />
      </View>
    </View>
  );
};

export { SidebarScrollable };

不滚动

scrollable={false} 时根节点退化成普通 View,用于外层已经有滚动容器、不希望嵌套滚动的场景。

SidebarNotScrollable.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const STATIC_ITEMS = ['概览', '明细', '设置'].map(title => ({ key: title, title }));

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarNotScrollable = () => {
  return (
    <View className="bg-background p-4">
      <View className="flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          items={STATIC_ITEMS}
          scrollable={false}
        />
        <Panel
          description="这一块没有固定高度,由侧边栏自身内容撑开"
          title="随内容撑开"
        />
      </View>
    </View>
  );
};

export { SidebarNotScrollable };

两种模式保持相同的节点层级(root 一层、content 一层),所以 classNameclassNames.content 落在哪个节点上不随 scrollable 变化。

样式覆盖

className 追加到根节点上,classNames 按 slot 细粒度覆盖:

slot作用位置
root根节点(scrollable 时即 ScrollView 本身)
content内容容器(scrollable 时是 contentContainer)
item单项(激活态换成 bg-background
itemText单项文字(激活态加粗)
indicator激活项左侧的指示条
SidebarCustomSlots.tsx
Loading…
import { Sidebar, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const CUSTOM_ITEMS = ['设计', '研发', '测试'].map(title => ({ key: title, title }));

/** 右侧内容区属性 */
interface PanelProps {
  /** 正文说明 */
  description: string;

  /** 面板标题 */
  title: string;
}

const Panel = (props: PanelProps) => {
  const { description, title } = props;

  return (
    <View className="flex-1 items-center justify-center gap-2 p-4">
      <Text className="text-base font-semibold">{title}</Text>
      <Text className="text-center text-sm text-muted-foreground">{description}</Text>
    </View>
  );
};

const SidebarCustomSlots = () => {
  return (
    <View className="bg-background p-4">
      <View className="h-56 flex-row overflow-hidden rounded-xl border border-border/60">
        <Sidebar
          className="self-stretch bg-muted/40"
          classNames={{
            content: 'py-2',
            indicator: 'h-10 w-1.5 rounded-sm bg-destructive',
            item: 'px-6 py-6',
            itemText: 'text-base',
            root: 'border-r border-destructive/20'
          }}
          items={CUSTOM_ITEMS}
        />
        <Panel
          description="root / content / indicator / item / itemText 均可独立覆写"
          title="自定义插槽"
        />
      </View>
    </View>
  );
};

export { SidebarCustomSlots };

指示器后于各项渲染,靠绘制顺序压在激活项之上 —— zIndex 在 Android 上不总可靠。

无障碍

根节点是 accessibilityRole="tablist",每项是 tab 并带上 selected / disabled 状态;字符串标题会自动成为 accessibilityLabel

API

除下表外,Sidebar 透传 View 的属性(styletestID 等,children 除外)。

属性说明类型默认值
items*侧边栏项数据SidebarItem[]-
activeIndex受控当前激活索引;索引是位置而非身份,items 变动后需自行校正number-
defaultActiveIndex非受控初始激活索引number0
onIndexChange激活项变化回调,同时给出该项配置,便于按 key 持久化选中态(index: number, item: SidebarItem) => void-
scrollable内容超出高度时是否可纵向滚动;false 时根节点退化成普通 Viewbooleantrue
className根节点类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<SidebarSlots>-

类型

import type { SidebarItem, SidebarProps, SidebarSlots } from '@skyroc/native-ui';

SidebarSlots

可通过 classNames 覆盖的 slot 名称。

'content' | 'indicator' | 'item' | 'itemText' | 'root'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 SidebarSlots。

Partial<Record<Slots, string>>

SidebarItem

侧边栏单项配置。

字段类型说明
key*string唯一标识。
title*ReactNode标题;字符串会自动包裹为 Text 并作为无障碍标签。
badgeReactNode徽标内容。
dotboolean是否显示小红点。
disabledboolean是否禁用该项。

包内还导出了 sidebarVariants