Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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/input-size-variants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@launchpad-ui/components': minor
---

Add a `size` prop to `Input` with `small` (24px tall) and `medium` (32px tall, the default) variants, matching the corresponding `Button` sizes so inputs and buttons align when placed side by side. `small` uses 11px text to match `Button`'s small height.
17 changes: 13 additions & 4 deletions packages/components/src/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@ import { useLPContextProps } from './utils';

const inputStyles = cva(styles.base, {
variants: {
size: {
small: styles.small,
medium: null,
},
variant: {
default: styles._default,
minimal: styles.minimal,
},
},
defaultVariants: {
size: 'medium',
variant: 'default',
},
});

interface InputVariants extends VariantProps<typeof inputStyles> {}
interface InputProps extends AriaInputProps, InputVariants {
Comment thread
cursor[bot] marked this conversation as resolved.
// `size` is omitted from the underlying React Aria props (the native numeric
// `size` HTML attribute) so it can be redefined as the design-system size scale.
interface InputProps extends Omit<AriaInputProps, 'size'>, InputVariants {
ref?: Ref<HTMLInputElement>;
}

Expand All @@ -37,14 +44,16 @@ const InputContext = createContext<ContextValue<InputProps, HTMLInputElement>>(n
*/
const Input = ({ ref, ...props }: InputProps) => {
[props, ref] = useLPContextProps(props, ref, InputContext);
const { variant = 'default' } = props;
// Pull the style-only variants out so they aren't forwarded to the DOM
// `<input>` (React Aria's Input spreads unknown props through unfiltered).
const { size = 'medium', variant = 'default', ...rest } = props;

return (
<AriaInput
{...props}
{...rest}
ref={ref}
className={composeRenderProps(props.className, (className, renderProps) =>
inputStyles({ ...renderProps, variant, className }),
inputStyles({ ...renderProps, size, variant, className }),
)}
/>
);
Expand Down
16 changes: 11 additions & 5 deletions packages/components/src/styles/Input.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
composes: field from './base.module.css';
font: var(--lp-text-body-2-regular);
color: var(--lp-color-text-ui-primary-base);
padding: 5px var(--lp-spacing-300);
height: var(--lp-size-32);
padding-inline: var(--lp-spacing-300);
border-radius: var(--lp-border-radius-medium);
border-width: var(--lp-border-width-200);
border-style: solid;
flex: 1;
flex: auto;
min-width: 0;

&:is([data-focused], [data-focus-within]) {
outline: 2px solid var(--lp-color-shadow-interactive-focus);
outline-offset: -2px;
outline: var(--lp-size-2) solid var(--lp-color-shadow-interactive-focus);
outline-offset: calc(-1 * var(--lp-size-2));
z-index: 1;
}

Expand All @@ -30,14 +31,19 @@
}
}

.small {
font: var(--lp-text-small-1-regular);
height: var(--lp-size-24);
}

._default {
border-color: var(--lp-color-border-field-base);
background-color: var(--lp-color-bg-field-base);
}

.minimal {
background-color: inherit;
margin-left: calc(-1 * var(--lp-spacing-300) - 1px);
margin-left: calc(-1 * var(--lp-spacing-300) - var(--lp-size-1));

&:not([data-invalid]) {
border-color: transparent;
Expand Down
58 changes: 58 additions & 0 deletions packages/components/stories/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/react-vite';

import { Input } from '../src/Input';

const meta: Meta<typeof Input> = {
title: 'Components/Forms/Input',
component: Input,
parameters: {
figma: {
design:
'https://www.figma.com/design/98HKKXL2dTle29ikJ3tzk7/%F0%9F%9A%80-LaunchPad?node-id=1-34456&m=dev',
},
docs: {
description: {
component: `
An input allows a user to input text.

Usually composed within a [TextField](/docs/components-forms-textfield--docs) alongside a [Label](/docs/components-content-label--docs) for an accessible field.
`,
},
},
},
};

export default meta;

type Story = StoryObj<typeof Input>;

export const Default: Story = {
args: {
'aria-label': 'Example',
defaultValue: 'Value',
},
};

/**
* Use the `size` prop to match the height of adjacent controls such as [Button](/docs/components-buttons-button--docs).
*/
export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Input aria-label="Small" size="small" defaultValue="Small" />
<Input aria-label="Medium" size="medium" defaultValue="Medium" />
</div>
),
};

/**
* The `minimal` variant removes the resting border for use in denser or inline layouts.
*/
export const Variants: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<Input aria-label="Default" variant="default" defaultValue="Default" />
<Input aria-label="Minimal" variant="minimal" defaultValue="Minimal" />
</div>
),
};
Loading