Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
406 changes: 406 additions & 0 deletions src/components/DatePicker/DatePicker.stories.tsx

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { type ComponentProps, type KeyboardEvent, type Ref, useRef } from 'react';

export type DatePickerSize = 'lg' | 'md' | 'sm';

export type DatePickerProps = Omit<ComponentProps<'div'>, 'children'> & {
size?: DatePickerSize;
isError?: boolean;
isDisabled?: boolean;
children: (props: {
yearRef: Ref<HTMLInputElement>;
monthRef: Ref<HTMLInputElement>;
dateRef: Ref<HTMLInputElement>;
}) => JSX.Element;
};

export const DatePicker = (props: DatePickerProps) => {
const { className, size = 'lg', isError, isDisabled, children, ...rest } = props;

const yearRef = useRef<HTMLInputElement>(null);
const monthRef = useRef<HTMLInputElement>(null);
const dateRef = useRef<HTMLInputElement>(null);

function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'ArrowRight') {
moveRight(event);
} else if (event.key === 'ArrowLeft') {
moveLeft(event);
} else if (event.key.match(/^[^0-9]$/)) {
if (!event.ctrlKey && !event.metaKey) {
event.preventDefault();
}
}
}

function moveRight(event: KeyboardEvent<HTMLInputElement>) {
const input = event.target as HTMLInputElement;
if (input.selectionStart !== input.selectionEnd) {
return;
}
if (input.selectionEnd === input.value.length) {
event.preventDefault();
if (input === yearRef.current) {
monthRef.current?.focus();
} else if (input === monthRef.current) {
dateRef.current?.focus();
}
}
}

function moveLeft(event: KeyboardEvent<HTMLInputElement>) {
const input = event.target as HTMLInputElement;
if (input.selectionStart !== input.selectionEnd) {
return;
}
if (input.selectionStart === 0) {
event.preventDefault();
if (input === monthRef.current) {
yearRef.current?.focus();
} else if (input === dateRef.current) {
monthRef.current?.focus();
}
}
}

return (
<div
className={`inline-flex h-14 -space-x-1 rounded-8 border border-solid-gray-600 bg-[--bg] p-0.5 pe-0 text-solid-gray-900 [--bg:theme(colors.white)] focus-within:border-black hover:border-solid-gray-900 data-[size=md]:h-12 data-[size=sm]:h-10 data-[disabled]:border-solid-gray-300 data-[error]:border-error-1 data-[disabled]:text-solid-gray-420 data-[disabled]:[--bg:theme(colors.solid-gray.50)] data-[error]:focus-within:border-red-1000 data-[error]:hover:border-red-1000 forced-colors:data-[disabled]:border-[GrayText] forced-colors:data-[disabled]:text-[GrayText] ${className ?? ''}`}
data-size={size}
data-error={isError || null}
data-disabled={isDisabled || null}
onKeyDown={handleKeyDown}
{...rest}
>
{children({ yearRef, monthRef, dateRef })}
</div>
);
};
5 changes: 5 additions & 0 deletions src/components/DatePicker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './DatePicker';
export * from './parts/DatePickerCalendarButton';
export * from './parts/DatePickerDate';
export * from './parts/DatePickerMonth';
export * from './parts/DatePickerYear';
33 changes: 33 additions & 0 deletions src/components/DatePicker/parts/DatePickerCalendarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { type ComponentProps, forwardRef } from 'react';
import type { DatePickerSize } from '../DatePicker';

export type DatePickerCalendarButtonProps = ComponentProps<'button'> & {
size?: DatePickerSize;
};

export const DatePickerCalendarButton = forwardRef<
HTMLButtonElement,
DatePickerCalendarButtonProps
>((props, ref) => {
const { className, size = 'lg', ...rest } = props;

return (
<button
className={`group flex h-14 items-center justify-center gap-x-1 rounded-6 border border-blue-900 bg-white px-3 text-blue-900 hover:border-[calc(3/16*1rem)] hover:px-2.5 focus-visible:outline focus-visible:outline-4 focus-visible:outline-offset-[calc(2/16*1rem)] focus-visible:outline-black focus-visible:ring-[calc(2/16*1rem)] focus-visible:ring-yellow-300 data-[size=md]:h-12 data-[size=sm]:h-10 ${className ?? ''}`}
type='button'
data-size={size}
ref={ref}
{...rest}
>
<svg width='24px' height='24px' viewBox='0 -960 960 960' role='img' aria-label='カレンダー'>
<path
d='M360-300q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Z'
fill='currentcolor'
/>
</svg>
<svg className='size-4 group-aria-expanded:rotate-180' viewBox='0 0 24 24' aria-hidden={true}>
<path d='M12 17.1L3 8L4 7L12 15L20 7L21 8L12 17.1Z' fill='currentcolor' />
</svg>
</button>
);
});
23 changes: 23 additions & 0 deletions src/components/DatePicker/parts/DatePickerDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type ComponentProps, forwardRef } from 'react';

export type DatePickerDateProps = ComponentProps<'input'> & {};

export const DatePickerDate = forwardRef<HTMLInputElement, DatePickerDateProps>((props, ref) => {
const { className, 'aria-disabled': disabled, readOnly, ...rest } = props;

return (
<label className='relative z-0 inline-flex flex-row-reverse last:pe-4 [&:has([aria-disabled="true"])]:pointer-events-none'>
<span className='relative z-10 self-center bg-[--bg] p-1 text-oln-16N-100'>日</span>
<input
className={`-me-1 w-11 rounded-8 border border-transparent bg-transparent pe-3 text-right focus:border-solid-gray-600 focus:outline focus:outline-4 focus:outline-offset-[calc(2/16*1rem)] focus:outline-black focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300 aria-disabled:pointer-events-none forced-colors:border-[Canvas] forced-colors:aria-disabled:focus:border-[GrayText] ${className ?? ''}`}
type='text'
inputMode='numeric'
pattern='\d+'
readOnly={disabled ? true : readOnly}
aria-disabled={disabled}
ref={ref}
{...rest}
/>
</label>
);
});
23 changes: 23 additions & 0 deletions src/components/DatePicker/parts/DatePickerMonth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type ComponentProps, forwardRef } from 'react';

export type DatePickerMonthProps = ComponentProps<'input'> & {};

export const DatePickerMonth = forwardRef<HTMLInputElement, DatePickerMonthProps>((props, ref) => {
const { className, 'aria-disabled': disabled, readOnly, ...rest } = props;

return (
<label className='relative z-0 inline-flex flex-row-reverse last:pe-4 [&:has([aria-disabled="true"])]:pointer-events-none'>
<span className='relative z-10 self-center bg-[--bg] p-1 text-oln-16N-100'>月</span>
<input
className={`-me-1 w-11 rounded-8 border border-transparent bg-transparent pe-3 text-right focus:border-solid-gray-600 focus:outline focus:outline-4 focus:outline-offset-[calc(2/16*1rem)] focus:outline-black focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300 aria-disabled:pointer-events-none forced-colors:border-[Canvas] forced-colors:aria-disabled:focus:border-[GrayText] ${className ?? ''}`}
type='text'
inputMode='numeric'
pattern='\d+'
readOnly={disabled ? true : readOnly}
aria-disabled={disabled}
ref={ref}
{...rest}
/>
</label>
);
});
23 changes: 23 additions & 0 deletions src/components/DatePicker/parts/DatePickerYear.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type ComponentProps, forwardRef } from 'react';

export type DatePickerYearProps = ComponentProps<'input'> & {};

export const DatePickerYear = forwardRef<HTMLInputElement, DatePickerYearProps>((props, ref) => {
const { className, 'aria-disabled': disabled, readOnly, ...rest } = props;

return (
<label className='relative z-0 inline-flex flex-row-reverse last:pe-4 [&:has([aria-disabled="true"])]:pointer-events-none'>
<span className='relative z-10 self-center bg-[--bg] p-1 text-oln-16N-100'>年</span>
<input
className={`-me-1 w-16 rounded-8 border border-transparent bg-transparent pe-3 text-right focus:border-solid-gray-600 focus:outline focus:outline-4 focus:outline-offset-[calc(2/16*1rem)] focus:outline-black focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300 aria-disabled:pointer-events-none forced-colors:border-[Canvas] forced-colors:aria-disabled:focus:border-[GrayText] ${className ?? ''}`}
type='text'
inputMode='numeric'
pattern='\d+'
readOnly={disabled ? true : readOnly}
aria-disabled={disabled}
ref={ref}
{...rest}
/>
</label>
);
});
17 changes: 10 additions & 7 deletions src/components/Divider/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import type { ComponentProps } from 'react';

export type DividerColor = 'gray-420' | 'gray-536' | 'black';

export const DividerColorStyle: { [key in DividerColor]: string } = {
'gray-420': 'border-solid-gray-420',
'gray-536': 'border-solid-gray-536',
black: 'border-black',
};

export type DividerProps = ComponentProps<'hr'> & {
color?: DividerColor;
};

export const Divider = (props: DividerProps) => {
const { className, color = 'gray-420', ...rest } = props;

return <hr className={`${DividerColorStyle[color]} ${className ?? ''}`} {...rest}></hr>;
return (
<hr
className={`
data-[color=gray-420]:border-solid-gray-420 data-[color=gray-536]:border-solid-gray-536 data-[color=black]:border-black
${className ?? ''}
`}
data-color={color}
{...rest}
></hr>
);
};
18 changes: 6 additions & 12 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@ import { type ComponentProps, forwardRef } from 'react';

export type InputBlockSize = 'lg' | 'md' | 'sm';

export const InputBlockSizeStyle: { [key in InputBlockSize]: string } = {
// NOTE:
// Tailwind CSS (v3.4.4) does not have any utility classes for logical properties of sizing.
// Once it is officially released, we will replace them with classes like `bs-14`.
lg: 'h-14',
md: 'h-12',
sm: 'h-10',
};

export type InputProps = ComponentProps<'input'> & {
isError?: boolean;
blockSize?: InputBlockSize;
Expand All @@ -22,13 +13,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return (
<input
className={`
max-w-full rounded-8 border bg-white px-4 py-3 text-oln-16N-100 text-solid-gray-800
${InputBlockSizeStyle[blockSize]}
${isError ? 'border-error-1' : 'border-solid-gray-900'}
max-w-full rounded-8 border bg-white px-4 py-3 border-solid-gray-600 text-oln-16N-100 text-solid-gray-800
hover:border-black
data-[size=sm]:h-10 data-[size=md]:h-12 data-[size=lg]:h-14
aria-[invalid=true]:border-error-1 aria-[invalid=true]:hover:border-red-1000
focus:outline focus:outline-4 focus:outline-black focus:outline-offset-[calc(2/16*1rem)] focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300
aria-disabled:border-solid-gray-300 aria-disabled:bg-solid-gray-50 aria-disabled:text-solid-gray-420 aria-disabled:pointer-events-none aria-disabled:forced-colors:text-[GrayText] aria-disabled:forced-colors:border-[GrayText]
${className ?? ''}
`}
aria-invalid={isError || undefined}
data-size={blockSize}
readOnly={props['aria-disabled'] ? true : readOnly}
ref={ref}
{...rest}
Expand Down
9 changes: 2 additions & 7 deletions src/components/Label/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ import type { ComponentProps } from 'react';

export type LabelSize = 'lg' | 'md' | 'sm';

export const labelSizeStyle: { [key in LabelSize]: string } = {
lg: 'text-std-18B-160',
md: 'text-std-17B-170',
sm: 'text-std-16B-170',
};

export type LabelProps = ComponentProps<'label'> & {
size?: LabelSize;
};
Expand All @@ -20,9 +14,10 @@ export const Label = (props: LabelProps) => {
<label
className={`
flex w-fit items-center gap-2 text-solid-gray-800
${labelSizeStyle[size]}
data-[size=sm]:text-std-16B-170 data-[size=md]:text-std-17B-170 data-[size=lg]:text-std-18B-160
${className ?? ''}
`}
data-size={size}
{...rest}
>
{children}
Expand Down
10 changes: 5 additions & 5 deletions src/components/LanguageSelector/parts/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export const LanguageSelectorMenu = forwardRef<HTMLUListElement, LanguageSelecto
return (
<ul
className={`
min-w-fit w-auto py-2 border border-solid-gray-420 bg-white shadow-1 rounded-8
has-[>:nth-child(7)]:rounded-r-none
${isCondensed ? 'max-h-[calc((32*6.5+16)/16*1rem)]' : 'max-h-[calc((44*6.5+16)/16*1rem)]'}
${className ?? ''}
`}
min-w-fit w-auto py-2 border border-solid-gray-420 bg-white shadow-1 rounded-8
has-[>:nth-child(7)]:rounded-r-none
${isCondensed ? 'max-h-[calc((32*6.5+16)/16*1rem)]' : 'max-h-[calc((44*6.5+16)/16*1rem)]'}
${className ?? ''}
`}
ref={ref}
{...rest}
>
Expand Down
9 changes: 2 additions & 7 deletions src/components/Legend/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@ import type { ComponentProps } from 'react';

export type LegendSize = 'lg' | 'md' | 'sm';

export const legendSizeStyle: { [key in LegendSize]: string } = {
lg: 'text-std-18B-160',
md: 'text-std-17B-170',
sm: 'text-std-16B-170',
};

export type LegendProps = ComponentProps<'legend'> & {
size?: LegendSize;
};
Expand All @@ -19,9 +13,10 @@ export const Legend = (props: LegendProps) => {
<legend
className={`
flex w-fit items-center gap-2 text-solid-gray-800
${legendSizeStyle[size]}
data-[size=sm]:text-std-16B-170 data-[size=md]:text-std-17B-170 data-[size=lg]:text-std-18B-160
${className ?? ''}
`}
data-size={size}
{...rest}
>
{children}
Expand Down
1 change: 1 addition & 0 deletions src/components/NotificationBanner/parts/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const NotificationBannerBody = (props: Props) => {
col-start-1 -col-end-1 desktop:col-start-2 text-std-16N-170 text-solid-gray-800
${className ?? ''}
`}
{...rest}
>
{children}
</div>
Expand Down
9 changes: 5 additions & 4 deletions src/components/RequirementBadge/RequirementBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ export const RequirementBadge = (props: RequirementBadgeProps) => {
return (
<span
className={`
text-oln-16N-100
${isOptional ? 'text-solid-gray-800' : 'text-red-800'}
${className ?? ''}
`}
text-oln-16N-100 text-red-800
data-[is-optional]:text-solid-gray-800
${className ?? ''}
`}
data-is-optional={isOptional || undefined}
{...rest}
>
{children}
Expand Down
18 changes: 6 additions & 12 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@ import { type ComponentProps, forwardRef } from 'react';

export type SelectBlockSize = 'lg' | 'md' | 'sm';

export const SelectBlockSizeStyle: { [key in SelectBlockSize]: string } = {
// NOTE:
// Tailwind CSS (v3.4.4) does not have any utility classes for logical properties of sizing.
// Once it is officially released, we will replace them with classes like `bs-14`.
lg: 'h-14',
md: 'h-12',
sm: 'h-10',
};

export type SelectProps = ComponentProps<'select'> & {
isError?: boolean;
blockSize?: SelectBlockSize;
Expand All @@ -33,13 +24,16 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>((props, ref) =>
<span className='relative'>
<select
className={`
w-full appearance-none border rounded-8 bg-white pl-4 pr-10 py-[calc(11/16*1rem)] text-oln-16N-100 text-solid-gray-800
${SelectBlockSizeStyle[blockSize]}
${isError ? 'border-error-1' : 'border-solid-gray-900'}
w-full appearance-none border border-solid-gray-600 rounded-8 bg-white pl-4 pr-10 py-[calc(11/16*1rem)] text-oln-16N-100 text-solid-gray-800
hover:border-black
data-[size=sm]:h-10 data-[size=md]:h-12 data-[size=lg]:h-14
aria-[invalid=true]:border-error-1 aria-[invalid=true]:hover:border-red-1000
focus:outline focus:outline-4 focus:outline-black focus:outline-offset-[calc(2/16*1rem)] focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300
aria-disabled:border-solid-gray-300 aria-disabled:bg-solid-gray-50 aria-disabled:text-solid-gray-420 aria-disabled:pointer-events-none aria-disabled:forced-colors:text-[GrayText] aria-disabled:forced-colors:border-[GrayText]
${className ?? ''}
`}
aria-invalid={isError || undefined}
data-size={blockSize}
onMouseDown={props['aria-disabled'] ? handleDisabledMouseDown : onMouseDown}
onKeyDown={props['aria-disabled'] ? handleDisabledKeyDown : onKeyDown}
ref={ref}
Expand Down
Loading