Skip to content
Draft
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: 4 additions & 1 deletion components/src/next/contexts/ComponentsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@
// limitations under the License.

import { createContext } from 'react';
import type { ComponentType, ReactNode, SVGProps } from 'react';
import type { ComponentType, ReactNode, RefAttributes, SVGProps } from 'react';

import type { AlertProps } from '../primitives/Alert';
import type { ButtonProps } from '../primitives/Button';
import type { SpinnerProps } from '../primitives/Spinner';
import type { StackProps } from '../primitives/Stack';

export interface PersesComponents {
Button: ComponentType<ButtonProps>;
Alert: ComponentType<AlertProps>;
Spinner: ComponentType<SpinnerProps>;
Box: ComponentType<BoxProps & RefAttributes<HTMLDivElement>>;
Stack: ComponentType<StackProps>;
}

export interface PersesIcons {
Expand Down
74 changes: 74 additions & 0 deletions components/src/next/primitives/Stack/Stack.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import type { Story } from '@ladle/react';

import { Stack } from './Stack';

export const Direction: Story = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
<div>
<h3>Direction: column (default)</h3>
<Stack spacing="md" style={{ border: '1px solid #ccc', padding: '1rem' }}>
<div style={{ background: '#eee', padding: '0.5rem' }}>Item 1</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>Item 2</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>Item 3</div>
</Stack>
</div>

<div>
<h3>Direction: row</h3>
<Stack direction="row" spacing="md" style={{ border: '1px solid #ccc', padding: '1rem' }}>
<div style={{ background: '#eee', padding: '0.5rem' }}>Item 1</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>Item 2</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>Item 3</div>
</Stack>
</div>
</div>
);
Direction.storyName = 'Direction';

export const SpacingTokens: Story = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((token) => (
<div key={token}>
<h3>spacing={'{token}'}: {token}</h3>
<Stack direction="row" spacing={token} style={{ border: '1px solid #ccc', padding: '0.5rem' }}>
<div style={{ background: '#eee', padding: '0.5rem' }}>A</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>B</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>C</div>
</Stack>
</div>
))}
</div>
);
SpacingTokens.storyName = 'Spacing Tokens';

export const AlignAndJustify: Story = () => (
<Stack
direction="row"
spacing="md"
alignItems="center"
justifyContent="space-between"
style={{ border: '1px solid #ccc', padding: '1rem', minHeight: '100px' }}
>
<div style={{ background: '#eee', padding: '0.5rem' }}>Short</div>
<div style={{ background: '#eee', padding: '1rem' }}>
Taller
<br />
content
</div>
<div style={{ background: '#eee', padding: '0.5rem' }}>End</div>
</Stack>
);
AlignAndJustify.storyName = 'Align and Justify';
92 changes: 92 additions & 0 deletions components/src/next/primitives/Stack/Stack.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { render, screen } from '@testing-library/react';
import type { ReactElement, ReactNode } from 'react';

import { ComponentsProvider } from '../../contexts/ComponentsProvider';
import { defaultComponents, defaultIcons } from '../defaults';
import { Stack } from './Stack';

function Wrapper({ children }: { children: ReactNode }): ReactElement {
return (
<ComponentsProvider components={defaultComponents} icons={defaultIcons}>
{children}
</ComponentsProvider>
);
}

describe('Stack', () => {
it('renders children', () => {
render(<Stack>Content</Stack>, { wrapper: Wrapper });
expect(screen.getByText('Content')).toBeInTheDocument();
});

it('applies the ps-Stack and ps-Box classes', () => {
render(<Stack data-testid="stack">Content</Stack>, { wrapper: Wrapper });
expect(screen.getByTestId('stack')).toHaveClass('ps-Stack');
expect(screen.getByTestId('stack')).toHaveClass('ps-Box');
});

it('merges additional className', () => {
render(
<Stack data-testid="stack" className="custom">
Content
</Stack>,
{ wrapper: Wrapper },
);
expect(screen.getByTestId('stack')).toHaveClass('custom');
});

it('defaults to a column direction', () => {
render(<Stack data-testid="stack">Content</Stack>, { wrapper: Wrapper });
expect(screen.getByTestId('stack')).toHaveStyle({ display: 'flex', flexDirection: 'column' });
});

it('supports row direction', () => {
render(
<Stack data-testid="stack" direction="row">
Content
</Stack>,
{ wrapper: Wrapper },
);
expect(screen.getByTestId('stack')).toHaveStyle({ flexDirection: 'row' });
});

it('resolves the spacing prop to a gap CSS variable', () => {
render(
<Stack data-testid="stack" spacing="lg">
Content
</Stack>,
{ wrapper: Wrapper },
);
expect(screen.getByTestId('stack')).toHaveStyle({ gap: 'var(--perses-spacing-lg)' });
});

it('forwards alignItems and justifyContent', () => {
render(
<Stack data-testid="stack" alignItems="center" justifyContent="space-between">
Content
</Stack>,
{ wrapper: Wrapper },
);
const stack = screen.getByTestId('stack');
expect(stack).toHaveStyle({ alignItems: 'center', justifyContent: 'space-between' });
});

it('forwards a ref to the underlying div', () => {
const ref = { current: null as HTMLDivElement | null };
render(<Stack ref={ref}>Content</Stack>, { wrapper: Wrapper });
expect(ref.current).toBeInstanceOf(HTMLDivElement);
});
});
45 changes: 45 additions & 0 deletions components/src/next/primitives/Stack/Stack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import clsx from 'clsx';
import { forwardRef } from 'react';

import { useComponents } from '../../contexts/ComponentsProvider';
import type { BoxProps } from '../Box/Box';

import './stack.css';

export interface StackProps extends Omit<BoxProps, 'display' | 'flexDirection' | 'gap'> {
direction?: 'row' | 'column';
spacing?: BoxProps['gap'];
}

export const Stack = forwardRef<HTMLDivElement, StackProps>(function Stack(
{ direction = 'column', spacing, className, ...rest },
ref,
) {
const {
components: { Box },
} = useComponents();

return (
<Box
ref={ref}
{...rest}
className={clsx('ps-Stack', className)}
display="flex"
flexDirection={direction}
gap={spacing}
/>
);
});
14 changes: 14 additions & 0 deletions components/src/next/primitives/Stack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export * from './Stack';
20 changes: 20 additions & 0 deletions components/src/next/primitives/Stack/stack.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright The Perses Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@layer perses.components {
.ps-Stack {
box-sizing: border-box;
}
}
3 changes: 2 additions & 1 deletion components/src/next/primitives/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
import { Button } from './Button';
import { ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from './Icon';
import { Spinner } from './Spinner';
import { Stack } from './Stack';

export const defaultComponents: PersesComponents = { Button, Alert, Spinner };
export const defaultComponents: PersesComponents = { Button, Alert, Spinner, Box, Stack };

Check failure on line 21 in components/src/next/primitives/defaults.ts

View workflow job for this annotation

GitHub Actions / test-npm

src/next/primitives/Stack/Stack.test.tsx

ReferenceError: Box is not defined ❯ src/next/primitives/defaults.ts:21:78 ❯ src/next/primitives/Stack/Stack.test.tsx:18:1

Check failure on line 21 in components/src/next/primitives/defaults.ts

View workflow job for this annotation

GitHub Actions / test-npm

src/next/primitives/Button/Button.test.tsx

ReferenceError: Box is not defined ❯ src/next/primitives/defaults.ts:21:78 ❯ src/next/primitives/Button/Button.test.tsx:19:1

Check failure on line 21 in components/src/next/primitives/defaults.ts

View workflow job for this annotation

GitHub Actions / test-npm

src/next/primitives/Alert/Alert.test.tsx

ReferenceError: Box is not defined ❯ src/next/primitives/defaults.ts:21:78 ❯ src/next/primitives/Alert/Alert.test.tsx:18:1

Check failure on line 21 in components/src/next/primitives/defaults.ts

View workflow job for this annotation

GitHub Actions / test-npm

src/next/contexts/ComponentsProvider.test.tsx

ReferenceError: Box is not defined ❯ src/next/primitives/defaults.ts:21:78 ❯ src/next/contexts/ComponentsProvider.test.tsx:19:1

export const defaultIcons: PersesIcons = {
Error: ErrorIcon,
Expand Down
6 changes: 5 additions & 1 deletion components/src/next/primitives/exports.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { Alert, Button, Icon, Spinner } from './index';
import { Alert, Box, Button, Icon, Spinner, Stack } from './index';
import type {
AlertProps,
AlertSeverity,
Expand All @@ -21,6 +21,7 @@ import type {
ButtonVariant,
IconProps,
SpinnerProps,
StackProps,
} from './index';

describe('primitives barrel exports', () => {
Expand All @@ -29,6 +30,7 @@ describe('primitives barrel exports', () => {
expect(Button).toBeDefined();
expect(Icon).toBeDefined();
expect(Spinner).toBeDefined();
expect(Stack).toBeDefined();
});

it('exports their prop types', () => {
Expand All @@ -40,6 +42,7 @@ describe('primitives barrel exports', () => {
const size: ButtonSize = 'md';
const iconProps: IconProps = {};
const spinnerProps: SpinnerProps = {};
const stackProps: StackProps = {};

expect(alertProps).toBeDefined();
expect(severity).toBe('info');
Expand All @@ -49,5 +52,6 @@ describe('primitives barrel exports', () => {
expect(size).toBe('md');
expect(iconProps).toBeDefined();
expect(spinnerProps).toBeDefined();
expect(stackProps).toBeDefined();
});
});
1 change: 1 addition & 0 deletions components/src/next/primitives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './Alert';
export * from './Button';
export * from './Icon';
export * from './Spinner';
export * from './Stack';
Loading