From 62abd275b750bf8e19e04f16463d1bb465e0ece3 Mon Sep 17 00:00:00 2001 From: fran-ink <171171801+fran-ink@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:54:33 -0500 Subject: [PATCH] feat: allow passing custom link to segmented control + add variable width prop + fix underline issue with button + fix local links in storybook --- src/components/Button/Button.tsx | 2 +- .../SegmentedControl.stories.tsx | 54 ++++++++++- .../SegmentedControl/SegmentedControl.tsx | 93 ++++++++++++------- src/global.d.ts | 2 + src/layout/InkLayout/InkLayout.stories.tsx | 14 ++- src/layout/InkLayout/InkNavLink.tsx | 4 +- 6 files changed, 129 insertions(+), 40 deletions(-) diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index efb227a..db18c1d 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -34,7 +34,7 @@ export const Button = ({ > = { +const meta: Meta> = { title: "Components/SegmentedControl", component: SegmentedControl, tags: ["autodocs"], @@ -33,6 +33,58 @@ export const Simple: Story = { args: {}, }; +export const VariableTabWidth: Story = { + args: { + variableTabWidth: true, + options: Array.from(new Array(5)).map((_, i) => ({ + selectedByDefault: i === 0, + label: (i + 1).toString().repeat(i + 1), + value: (i + 1).toString(), + })), + }, +}; + export const DisplayOnDarkBackground: Story = { args: { displayOn: "dark" }, }; + +export const AsLinks: Story = { + args: { + options: [ + { + label: "First", + value: "first", + selectedByDefault: true, + props: { + as: "a", + asProps: { + href: "#first", + target: "_self", + }, + }, + }, + { + label: "Second", + value: "second", + props: { + as: "a", + asProps: { + href: "#second", + target: "_self", + }, + }, + }, + { + label: "Third", + value: "third", + props: { + as: "a", + asProps: { + href: "#third", + target: "_self", + }, + }, + }, + ], + }, +}; diff --git a/src/components/SegmentedControl/SegmentedControl.tsx b/src/components/SegmentedControl/SegmentedControl.tsx index abb6da8..d1137ad 100644 --- a/src/components/SegmentedControl/SegmentedControl.tsx +++ b/src/components/SegmentedControl/SegmentedControl.tsx @@ -1,26 +1,41 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { classNames, variantClassNames } from "../../util/classes"; import { DisplayOnProps } from "../../util/theme"; +import { PolymorphicDefinition } from "../polymorphic"; -export interface SegmentedControlProps - extends DisplayOnProps { - options: SegmentedControlOption[]; - onOptionChange: (option: SegmentedControlOption, index: number) => void; -} +export type SegmentedControlProps< + TOptionValue extends string, + TButtonAs extends React.ElementType = "button", +> = DisplayOnProps & { + options: SegmentedControlOption[]; + onOptionChange: ( + option: SegmentedControlOption, + index: number + ) => void; + variableTabWidth?: boolean; +}; -export interface SegmentedControlOption { +export interface SegmentedControlOption< + TOptionValue extends string, + TButtonAs extends React.ElementType = "button", +> { label: React.ReactNode; - value: T; + value: TOptionValue; selectedByDefault?: boolean; + props?: PolymorphicDefinition; } -export const SegmentedControl = ({ +export const SegmentedControl = < + TOptionValue extends string, + TButtonAs extends React.ElementType = "button", +>({ options, onOptionChange, + variableTabWidth, displayOn = "light", -}: SegmentedControlProps) => { +}: SegmentedControlProps) => { const itemsRef = useRef>([]); - const [selectedOption, setSelectedOption] = useState( + const [selectedOption, setSelectedOption] = useState( options.find((opt) => opt.selectedByDefault)?.value ?? null ); const selectedIndex = useMemo( @@ -73,33 +88,47 @@ export const SegmentedControl = ({ )}
- {options.map((option, index) => ( - - ))} + {options.map((option, index) => { + const { as, asProps } = option.props ?? {}; + + const ButtonComponent = as ?? "button"; + + return ( + { + itemsRef.current[index] = el; + }} + key={option.value} + onClick={(event) => { + setSelectedOption(option.value); + onOptionChange(option, index); + asProps?.onClick?.(event); + }} + draggable={false} + > + {option.label} + + ); + })}
); diff --git a/src/global.d.ts b/src/global.d.ts index 0927eb1..a06eb86 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,3 +4,5 @@ declare module "*?base64" { const value: string; export default value; } + +type StringWithAutocomplete = T | (string & Record); diff --git a/src/layout/InkLayout/InkLayout.stories.tsx b/src/layout/InkLayout/InkLayout.stories.tsx index 9714441..eb5bdaf 100644 --- a/src/layout/InkLayout/InkLayout.stories.tsx +++ b/src/layout/InkLayout/InkLayout.stories.tsx @@ -9,13 +9,15 @@ const SideNav = () => { links={[ { label: "Home", - href: "/", + href: "#home", icon: , + target: "_self", }, { label: "Settings", - href: "/settings", + href: "#settings", icon: , + target: "_self", }, ]} /> @@ -64,18 +66,20 @@ export const SideNavWithCustomButtons: Story = { , + target: "_self", }, { label: "Settings", - href: "/settings", + href: "#settings", icon: , + target: "_self", }, ]} /> diff --git a/src/layout/InkLayout/InkNavLink.tsx b/src/layout/InkLayout/InkNavLink.tsx index 7f0c90f..560c8f8 100644 --- a/src/layout/InkLayout/InkNavLink.tsx +++ b/src/layout/InkLayout/InkNavLink.tsx @@ -4,10 +4,11 @@ import { classNames } from "../../util/classes"; const DEFAULT_COMPONENT_TYPE = "a" as const; -export interface InkLayoutLink { +export interface InkLayoutLink extends React.ComponentPropsWithoutRef<"a"> { label: string; href: string; icon: React.ReactNode; + target?: StringWithAutocomplete<"_blank" | "_self">; } export type InkNavLinkProps< @@ -38,6 +39,7 @@ export const InkNavLink = < "ink:flex ink:items-center ink:gap-1.5 ink:px-1.5 ink:py-1.5 ink:text-inherit ink:no-underline ink:rounded-md ink:transition-colors ink:duration-200 ink:hover:bg-background-container", className )} + draggable={false} {...asProps} {...props} >