Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into feat/checkbox
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas.J.Han <[email protected]>
  • Loading branch information
lukasjhan committed Aug 20, 2024
2 parents fca13b4 + 61f799f commit 7a8fd7d
Show file tree
Hide file tree
Showing 7 changed files with 632 additions and 0 deletions.
97 changes: 97 additions & 0 deletions packages/core/lib/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from 'react';
import { Label } from './Label';

interface BreadcrumbItem {
label: string;
onClick: () => void;
}

interface BreadcrumbProps {
items: BreadcrumbItem[];
maxLength?: number;
}

const ChevronIcon: React.FC = () => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M6 4L10 8L6 12"
stroke="currentColor"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

const EllipsisIcon: React.FC = () => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<circle cx="3" cy="8" r="1" fill="currentColor" />
<circle cx="8" cy="8" r="1" fill="currentColor" />
<circle cx="13" cy="8" r="1" fill="currentColor" />
</svg>
);

export const Breadcrumb: React.FC<BreadcrumbProps> = ({
items,
maxLength = 3,
}) => {
const maxItems = Math.max(2, maxLength);
const renderItems = () => {
if (items.length <= maxItems) {
return items;
}

const visibleItems = [];
visibleItems.push(items[0]);

if (maxItems > 2) {
for (let i = 1; i < maxItems - 1; i++) {
visibleItems.push(items[items.length - maxItems + i]);
}
}

visibleItems.push(items[items.length - 1]);

return visibleItems;
};

return (
<nav aria-label="브레드크럼" className="py-2">
<ol className="flex items-center">
{renderItems().map((item, index) => (
<li key={index} className="flex items-center m-0">
{index > 0 && <ChevronIcon />}
{index === 1 && items.length > maxItems && (
<>
<EllipsisIcon />
<ChevronIcon />
</>
)}
<Label
onClick={item.onClick}
color={'gray-90'}
size="xs"
className="hover:underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary cursor-pointer"
>
{item.label}
</Label>
</li>
))}
</ol>
</nav>
);
};
79 changes: 79 additions & 0 deletions packages/core/lib/components/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { forwardRef, useState } from 'react';
import { Label } from './Label';

type TextAreaProps = {
id: string;
title?: string;
description?: string;
size?: 'small' | 'medium' | 'large';
maxLength?: number;
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;

export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
(
{
title,
description,
id,
placeholder,
size = 'medium',
maxLength,
onChange,
...props
},
ref
) => {
const [charCount, setCharCount] = useState(0);

const sizeClasses = {
small: 'h-24',
medium: 'h-32',
large: 'h-40',
}[size];

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setCharCount(e.target.value.length);
if (onChange) {
onChange(e);
}
};

return (
<div className="flex flex-col gap-1 justify-center">
{title && (
<Label htmlFor={id} weight="bold">
{title}
</Label>
)}
{description && (
<Label size={'s'} color={'gray-50'}>
{description}
</Label>
)}
<div className="relative">
<textarea
ref={ref}
id={id}
className={`
w-full ${sizeClasses} px-4 py-3 text-gray-70 border rounded-4
focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary mt-3
border-gray-50 transition duration-150 ease-in-out resize-none
`}
placeholder={placeholder}
maxLength={maxLength}
onChange={handleChange}
{...props}
/>
{maxLength && (
<div className="flex justify-end gap-1">
<Label color={'primary'} size={'xs'}>
{charCount}
</Label>
<Label size={'xs'}>{`/${maxLength}`}</Label>
</div>
)}
</div>
</div>
);
}
);
85 changes: 85 additions & 0 deletions packages/core/lib/components/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { forwardRef } from 'react';
import { Label } from './Label';

type TextInputProps = {
id: string;
title?: string;
description?: string;
helpText?: string;
error?: string;
length?: 'x-short' | 'short' | 'middle' | 'long' | 'full';
} & React.InputHTMLAttributes<HTMLInputElement>;

export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
(
{
title,
description,
helpText,
error,
id,
placeholder,
length = 'middle',
...props
},
ref
) => {
const inputId = id;
const helperTextId = `${inputId}-help`;
const errorId = `${inputId}-error`;

const lengthClasses = {
'x-short': 'w-16',
short: 'w-32',
middle: 'w-64',
long: 'w-128',
full: 'w-full',
}[length];

return (
<div className="flex flex-col gap-1 justify-center">
{title && (
<Label htmlFor={inputId} weight="bold">
{title}
</Label>
)}
{description && (
<Label size={'s'} color={'gray-50'}>
{description}
</Label>
)}
<div className="relative">
<input
ref={ref}
id={inputId}
type="text"
className={`
${lengthClasses} px-4 py-3 text-gray-70 border rounded-2 focus:border-primary
focus:outline-none focus:ring-1 focus:ring-primary mt-3
${error ? 'border-danger' : 'border-gray-50'}
transition duration-150 ease-in-out
`}
placeholder={placeholder}
aria-describedby={error ? errorId : helperTextId}
aria-invalid={error ? 'true' : 'false'}
{...props}
/>
</div>
{error ? (
<Label id={errorId} size={'s'} color={'danger'} className="mt-1">
{error}
</Label>
) : helpText ? (
<Label
id={helperTextId}
size={'s'}
color={'gray-50'}
className="mt-1"
>
{helpText}
</Label>
) : null}
</div>
);
}
);
6 changes: 6 additions & 0 deletions packages/core/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { LinkButton } from './components/LinkButton';
import { Tag } from './components/Tag';
import { Spinner } from './components/Spinner';
import { Badge } from './components/Badge';
import { TextInput } from './components/TextInput';
import { TextArea } from './components/TextArea';
import { Breadcrumb } from './components/Breadcrumb';
import { Switch } from './components/Switch';
import { Chip } from './components/Chip';
import { Checkbox } from './components/Checkbox';
Expand All @@ -29,4 +32,7 @@ export {
Chip,
Checkbox,
RadioButtonGroup,
TextInput,
TextArea,
Breadcrumb,
};
63 changes: 63 additions & 0 deletions stories/core/Breadcrumb.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Breadcrumb } from '../../packages/core/lib';

const meta = {
title: 'Components/Breadcrumb',
component: Breadcrumb,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof Breadcrumb>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
items: [
{ label: '홈', onClick: () => console.log('홈') },
{ label: '문서', onClick: () => console.log('문서') },
{ label: '서적', onClick: () => console.log('서적') },
{ label: '서적 서적', onClick: () => console.log('서적 서적') },
],
},
};

export const MaxItems: Story = {
args: {
items: [
{ label: '홈', onClick: () => console.log('홈') },
{ label: '문서', onClick: () => console.log('문서') },
{ label: '서적', onClick: () => console.log('서적') },
{ label: '서적 서적', onClick: () => console.log('서적 서적') },
],
maxLength: 2,
},
};

export const WithoutEllipsis: Story = {
args: {
items: [
{ label: '홈', onClick: () => console.log('홈') },
{ label: '문서', onClick: () => console.log('문서') },
{ label: '서적', onClick: () => console.log('서적') },
{ label: '서적 서적', onClick: () => console.log('서적 서적') },
],
maxLength: 5,
},
};

export const maxLengthIsLessThan2: Story = {
args: {
items: [
{ label: '홈', onClick: () => console.log('홈') },
{ label: '문서', onClick: () => console.log('문서') },
{ label: '서적', onClick: () => console.log('서적') },
{ label: '서적 서적', onClick: () => console.log('서적 서적') },
],
maxLength: 1,
},
};
Loading

0 comments on commit 7a8fd7d

Please sign in to comment.