Skip to content

Commit

Permalink
feat: allow passing custom link to segmented control
Browse files Browse the repository at this point in the history
+ add variable width prop
+ fix underline issue with button
+ fix local links in storybook
  • Loading branch information
fran-ink committed Nov 25, 2024
1 parent 8f3ed90 commit 62abd27
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const Button = <T extends ElementType = typeof DEFAULT_BUTTON_TAG>({
<Component
className={classNames(
"ink:rounded-full ink:font-bold ink:font-default ink:transition-colors ink:hover:cursor-pointer ink:disabled:cursor-not-allowed ink:duration-100",
"ink:flex ink:items-center ink:justify-center ink:gap-1 ink:select-none",
"ink:flex ink:items-center ink:justify-center ink:gap-1 ink:select-none ink:no-underline",
variantClassNames(size, {
sm: "ink:px-3 ink:py-2 ink:text-body-2",
md: "ink:px-4 ink:py-3 ink:text-h4",
Expand Down
54 changes: 53 additions & 1 deletion src/components/SegmentedControl/SegmentedControl.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { SegmentedControl, SegmentedControlProps } from "./SegmentedControl";

const meta: Meta<SegmentedControlProps<string>> = {
const meta: Meta<SegmentedControlProps<string, "button" | "a">> = {
title: "Components/SegmentedControl",
component: SegmentedControl,
tags: ["autodocs"],
Expand Down Expand Up @@ -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",
},
},
},
],
},
};
93 changes: 61 additions & 32 deletions src/components/SegmentedControl/SegmentedControl.tsx
Original file line number Diff line number Diff line change
@@ -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<T extends string>
extends DisplayOnProps {
options: SegmentedControlOption<T>[];
onOptionChange: (option: SegmentedControlOption<T>, index: number) => void;
}
export type SegmentedControlProps<
TOptionValue extends string,
TButtonAs extends React.ElementType = "button",
> = DisplayOnProps & {
options: SegmentedControlOption<TOptionValue, TButtonAs>[];
onOptionChange: (
option: SegmentedControlOption<TOptionValue, TButtonAs>,
index: number
) => void;
variableTabWidth?: boolean;
};

export interface SegmentedControlOption<T extends string> {
export interface SegmentedControlOption<
TOptionValue extends string,
TButtonAs extends React.ElementType = "button",
> {
label: React.ReactNode;
value: T;
value: TOptionValue;
selectedByDefault?: boolean;
props?: PolymorphicDefinition<TButtonAs>;
}

export const SegmentedControl = <T extends string>({
export const SegmentedControl = <
TOptionValue extends string,
TButtonAs extends React.ElementType = "button",
>({
options,
onOptionChange,
variableTabWidth,
displayOn = "light",
}: SegmentedControlProps<T>) => {
}: SegmentedControlProps<TOptionValue, TButtonAs>) => {
const itemsRef = useRef<Array<HTMLButtonElement | null>>([]);
const [selectedOption, setSelectedOption] = useState<T | null>(
const [selectedOption, setSelectedOption] = useState<TOptionValue | null>(
options.find((opt) => opt.selectedByDefault)?.value ?? null
);
const selectedIndex = useMemo(
Expand Down Expand Up @@ -73,33 +88,47 @@ export const SegmentedControl = <T extends string>({
)}
<div
className={classNames(
"ink:grid ink:gap-2 ink:h-6 ink:grid-flow-col ink:[grid-auto-columns:1fr] ink:text-body-2 ink:font-bold ink:rounded-full",
"ink:grid ink:h-6 ink:grid-flow-col ink:text-body-2 ink:font-bold ink:rounded-full",
variantClassNames(displayOn, {
light: "ink:bg-background-container",
dark: "ink:bg-background-light",
})
}),
variableTabWidth
? "ink:[grid-auto-columns:auto]"
: "ink:[grid-auto-columns:1fr]"
)}
>
{options.map((option, index) => (
<button
className={classNames(
"ink:px-4 ink:h-full ink:box-border ink:rounded-full ink:relative ink:z-10 ink:transition-colors ink:duration-200 ink:hover:cursor-pointer ink:select-none",
selectedOption === option.value
? "ink:text-text-default"
: "ink:text-text-on-secondary"
)}
ref={(el) => {
itemsRef.current[index] = el;
}}
key={option.value}
onClick={() => {
setSelectedOption(option.value);
onOptionChange(option, index);
}}
>
{option.label}
</button>
))}
{options.map((option, index) => {
const { as, asProps } = option.props ?? {};

const ButtonComponent = as ?? "button";

return (
<ButtonComponent
{...asProps}
className={classNames(
"ink:h-full ink:box-border ink:rounded-full ink:relative ink:z-10 ink:transition-colors ink:duration-200 ink:hover:cursor-pointer ink:select-none ink:no-underline ink:flex ink:items-center ink:justify-center",
selectedOption === option.value
? "ink:text-text-default"
: "ink:text-text-on-secondary",
variableTabWidth ? "ink:px-3" : "ink:px-4",
asProps?.className
)}
ref={(el) => {
itemsRef.current[index] = el;
}}
key={option.value}
onClick={(event) => {
setSelectedOption(option.value);
onOptionChange(option, index);
asProps?.onClick?.(event);
}}
draggable={false}
>
{option.label}
</ButtonComponent>
);
})}
</div>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ declare module "*?base64" {
const value: string;
export default value;
}

type StringWithAutocomplete<T> = T | (string & Record<never, never>);
14 changes: 9 additions & 5 deletions src/layout/InkLayout/InkLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ const SideNav = () => {
links={[
{
label: "Home",
href: "/",
href: "#home",
icon: <InkIcon.Home />,
target: "_self",
},
{
label: "Settings",
href: "/settings",
href: "#settings",
icon: <InkIcon.Settings />,
target: "_self",
},
]}
/>
Expand Down Expand Up @@ -64,18 +66,20 @@ export const SideNavWithCustomButtons: Story = {
<InkLayoutSideNav
linkAs={{
as: Button,
asProps: { variant: "primary", as: "a", target: "_blank" },
asProps: { variant: "primary", as: "a", target: "_self" },
}}
links={[
{
label: "Home",
href: "/",
href: "#home",
icon: <InkIcon.Home />,
target: "_self",
},
{
label: "Settings",
href: "/settings",
href: "#settings",
icon: <InkIcon.Settings />,
target: "_self",
},
]}
/>
Expand Down
4 changes: 3 additions & 1 deletion src/layout/InkLayout/InkNavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -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}
>
Expand Down

0 comments on commit 62abd27

Please sign in to comment.