Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/base-ui-1-8-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vapor-ui/core': patch
---

update `@base-ui/react` to 1.8.0 — support `keepMounted` on `Avatar` with stacked image/fallback styles, accept functional updater in Toast `update`
2 changes: 1 addition & 1 deletion apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@base-ui/react": "^1.7.0",
"@base-ui/react": "^1.8.0",
"@radix-ui/react-collapsible": "^1.1.20",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-radio-group": "^1.4.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,36 @@
"props": [
{
"name": "className",
"type": ["string", "(state: State) => string"],
"type": [
"string",
"(state: State) => string"
],
"required": false,
"description": "요소에 적용된 CSS 클래스 또는 컴포넌트의 상태에 따라 클래스를 반환하는 함수."
},
{
"name": "render",
"type": ["ReactElement"],
"type": [
"ReactElement"
],
"required": false,
"description": "컴포넌트의 HTML 요소를 다른 태그로 대체하거나 다른 컴포넌트와 조합할 수 있습니다. 렌더링할 요소를 반환하는 `ReactElement` 또는 함수를 인수로 받습니다."
},
{
"name": "onLoadingStatusChange",
"type": ["(status: ImageLoadingStatus) => void"],
"type": [
"(status: ImageLoadingStatus) => void"
],
"required": false,
"description": "로딩 상태가 변경될 때 호출되는 콜백"
},
{
"name": "keepMounted",
"type": [
"boolean"
],
"required": false,
"description": "이미지 요소를 미리 로드하지 않고 마운트된 상태로 유지하며 제자리에서 로드합니다. `loading=\"lazy\"`와 `next/image` 같은 최적화 이미지 컴포넌트를 지원합니다. 기본값은 `false`입니다."
}
]
}
2 changes: 1 addition & 1 deletion packages/composites/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@base-ui/utils": "^0.3.2",
"@base-ui/utils": "^0.4.0",
"@vapor-ui/icons": "workspace:^",
"clsx": "^2.1.1"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@base-ui/react": "^1.7.0",
"@base-ui/utils": "^0.3.2",
"@base-ui/react": "^1.8.0",
"@base-ui/utils": "^0.4.0",
"@vanilla-extract/css": "^1.21.2",
"@vanilla-extract/css-utils": "^0.1.6",
"@vanilla-extract/dynamic": "^2.1.5",
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/components/avatar/avatar.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const radii = createVar('border-radius');

export const root = componentRecipe({
base: {
position: 'relative',
display: 'inline-flex',
flexShrink: 0,
alignItems: 'center',
Expand Down Expand Up @@ -60,6 +61,8 @@ export const fallbackBgVar = createVar('fallback-background-color');

export const fallback = componentRecipe({
base: {
position: 'absolute',
inset: 0,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
Expand Down Expand Up @@ -105,10 +108,18 @@ export const fallback = componentRecipe({
});

export const image = componentStyle({
position: 'absolute',
inset: 0,
display: 'inline',
objectFit: 'cover',
width: '100%',
height: '100%',

selectors: {
'&[data-loading], &[data-error]': {
visibility: 'hidden',
},
},
});

export type RootVariants = NonNullable<RecipeVariants<typeof root>>;
Expand Down
93 changes: 93 additions & 0 deletions packages/core/src/components/avatar/avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,96 @@ export const TestBed: Story = {
</>
),
};

/* -------------------------------------------------------------------------------------------------
* keepMounted — 스타일 검증용
* -----------------------------------------------------------------------------------------------*/

const BROKEN_URL = 'https://example.invalid/not-an-image.png';

/**
* keepMounted를 켜면 로딩 중·에러 시 image와 fallback이 DOM에 공존한다.
* 현재 avatar.css.ts는 둘 다 positioned가 아니라 이 공존을 전제하지 않는다.
* A~D를 나란히 비교해 어떤 CSS가 필요한지 눈으로 확인하는 스토리.
*/
const STACK_STYLES = `
.kmStack { position: relative; }
.kmStack > * { position: absolute; inset: 0; }
.kmHide[data-loading],
.kmHide[data-error] { visibility: hidden; }
`;

const Row = ({
label,
rootClassName,
imageClassName,
keepMounted,
delay,
}: {
label: string;
rootClassName?: string;
imageClassName?: string;
keepMounted?: boolean;
delay?: number;
}) => (
<Flex flexDirection="column" gap="$100">
<strong style={{ fontSize: 12 }}>{label}</strong>
<Flex gap="$200" alignItems="center">
{(
[
['loaded', IMAGE_URL],
['error', BROKEN_URL],
['no src', undefined],
] as const
).map(([state, src]) => (
<Flex key={state} flexDirection="column" alignItems="center" gap="$050">
<Avatar.Root
size="xl"
shape="circle"
src={src}
alt="noah.choi"
keepMounted={keepMounted}
delay={delay}
className={rootClassName}
>
<Avatar.ImagePrimitive className={imageClassName} />
<Avatar.FallbackPrimitive />
</Avatar.Root>
<span style={{ fontSize: 11, opacity: 0.6 }}>{state}</span>
</Flex>
))}
</Flex>
</Flex>
);

export const KeepMounted: Story = {
render: () => (
<>
<style>{STACK_STYLES}</style>
<Flex flexDirection="column" gap="$300">
<Row label="A. keepMounted + 현재 스타일 (겹치기 없음)" keepMounted />
<Row label="B. keepMounted + 겹치기만" keepMounted rootClassName="kmStack" />
<Row
label="C. keepMounted + 겹치기 + data-loading/data-error 숨김"
keepMounted
rootClassName="kmStack"
imageClassName="kmHide"
/>
<Row
label="E. keepMounted + 겹치기만 + delay 600 — fallback이 지연되는 구간"
keepMounted
delay={600}
rootClassName="kmStack"
/>
<Row
label="F. keepMounted + 겹치기 + 숨김 + delay 600"
keepMounted
delay={600}
rootClassName="kmStack"
imageClassName="kmHide"
/>
<Row label="D. 참조 — keepMounted 없음 (현재 기본 동작)" />
</Flex>
</>
),
};
2 changes: 2 additions & 0 deletions packages/core/src/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const AvatarRoot = forwardRef<HTMLSpanElement, AvatarRoot.Props>((props,
'height',
'loading',
'onLoadingStatusChange',
'keepMounted',
'referrerPolicy',
'sizes',
'srcSet',
Expand Down Expand Up @@ -108,6 +109,7 @@ export const AvatarImagePrimitive = forwardRef<HTMLImageElement, AvatarImagePrim
'height',
'loading',
'onLoadingStatusChange',
'keepMounted',
'referrerPolicy',
'sizes',
'srcSet',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe('<NavigationMenu.Root />', () => {
expect(screen.getByText(PANEL_LINK_1)).toBeInTheDocument();
});

it('should focus the first item when the menu is opened via ArrowDown key', async () => {
it('should keep focus on the trigger when the menu is opened via ArrowDown key and move focus to the first item on Tab', async () => {
const rendered = render(<NavigationMenuTest aria-label="Main" />);
const trigger = rendered.getByRole('button', { name: TRIGGER_1 });

Expand All @@ -191,6 +191,10 @@ describe('<NavigationMenu.Root />', () => {
await userEvent.keyboard('[ArrowDown]');

expect(screen.getByText(PANEL_LINK_1)).toBeInTheDocument();
expect(trigger).toHaveFocus();

await userEvent.tab();

expect(screen.getByText(PANEL_LINK_1)).toHaveFocus();
});

Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/components/toast/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ type ToastOptions = { icon?: ReactElement<unknown>; close?: boolean; actionProps

type ToastProps = ToastVariants & ToastOptions;

type BaseToastObject<Data extends object> = Partial<BaseToast.Root.ToastObject<Data>>;
type BaseToastObject<Data extends object> = BaseToast.Root.ToastObject<Data>;
type ToastObject<Data extends object> = Omit<BaseToastObject<Data>, 'type' | 'actionProps'>;

type ToastObjectType<Data extends object> = ToastObject<Data> & ToastProps;
Expand All @@ -332,7 +332,10 @@ export interface ToastManagerAddOptions<Data extends object> extends Omit<
}

export interface ToastManagerUpdateOptions<Data extends object> extends Partial<
ToastManagerAddOptions<Data>
Omit<
ToastObjectType<Data>,
'id' | 'ref' | 'height' | 'transitionStatus' | 'limited' | 'updateKey'
>
> {}

export interface ToastManagerPromiseOptions<Value, Data extends object> extends BasePromiseOptions {
Expand All @@ -355,7 +358,12 @@ export interface ToastManager extends BaseToastManager {
}) => void,
) => () => void;
add: <Data extends object>(options: ToastManagerAddOptions<Data>) => string;
update: <Data extends object>(id: string, options: ToastManagerUpdateOptions<Data>) => void;
update: <Data extends object>(
id: string,
options:
| ToastManagerUpdateOptions<Data>
| ((prevToast: ToastObjectType<Data>) => ToastManagerUpdateOptions<Data>),
) => void;
close: (id?: string) => void;
promise: <Value, Data extends object>(
promise: Promise<Value>,
Expand Down
42 changes: 24 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading