-
-
Notifications
You must be signed in to change notification settings - Fork 9.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI: Create new form elements in the new Core UI (Input, TextArea, Select) #23469
Changes from 8 commits
312c23f
f56ded7
f9b9e9a
be6ad7b
42ba728
4d2bcad
d3dad50
4a21186
a164fd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { Input } from './Input'; | ||
|
||
const meta: Meta<typeof Input> = { | ||
title: 'Input', | ||
component: Input, | ||
tags: ['autodocs'], | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof Input>; | ||
|
||
export const Base: Story = { | ||
args: { | ||
placeholder: 'Hello World', | ||
}, | ||
}; | ||
|
||
export const Filled: Story = { | ||
args: { | ||
...Base.args, | ||
value: 'Hello World', | ||
}, | ||
}; | ||
|
||
export const Disabled: Story = { | ||
args: { | ||
...Base.args, | ||
disabled: true, | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import React, { forwardRef } from 'react'; | ||
import { styled } from '@storybook/theming'; | ||
|
||
interface InputProps { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding PropTypes for better type checking and to make the code more self-documenting. [medium] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. euh ... no. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a lie, it's actually more!
|
||
disabled?: boolean; | ||
placeholder?: string; | ||
value?: string; | ||
} | ||
|
||
export const Input = forwardRef<HTMLInputElement, InputProps>(({ ...props }, ref) => { | ||
return <StyledInput ref={ref} {...props} />; | ||
}); | ||
Comment on lines
+10
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok |
||
|
||
Input.displayName = 'Input'; | ||
|
||
const StyledInput = styled.input(({ theme }) => ({ | ||
// resets | ||
appearance: 'none', | ||
border: '0 none', | ||
display: 'block', | ||
margin: ' 0', | ||
position: 'relative', | ||
|
||
// styles | ||
width: '100%', | ||
height: '32px', | ||
transition: 'box-shadow 200ms ease-out, opacity 200ms ease-out', | ||
color: theme.input.color, | ||
background: theme.input.background, | ||
boxShadow: `${theme.input.border} 0 0 0 1px inset`, | ||
borderRadius: theme.input.borderRadius, | ||
fontSize: theme.typography.size.s2 - 1, | ||
padding: '6px 10px', | ||
boxSizing: 'border-box', | ||
lineHeight: '20px', | ||
|
||
'&:focus': { | ||
boxShadow: `${theme.color.secondary} 0 0 0 1px inset`, | ||
outline: 'none', | ||
}, | ||
|
||
'&[disabled]': { | ||
cursor: 'not-allowed', | ||
opacity: 0.5, | ||
}, | ||
|
||
'&:-webkit-autofill': { | ||
WebkitBoxShadow: `0 0 0 3em ${theme.color.lightest} inset`, | ||
}, | ||
|
||
'&::placeholder': { | ||
color: theme.textMutedColor, | ||
opacity: 1, | ||
}, | ||
})); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import React from 'react'; | ||
|
||
import { Select } from './Select'; | ||
|
||
const meta: Meta<typeof Select.Root> = { | ||
title: 'Select', | ||
component: Select.Root, | ||
tags: ['autodocs'], | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof Select.Root>; | ||
|
||
export const Base: Story = { | ||
args: {}, | ||
render: (_, { args }) => ( | ||
<div style={{ width: 400 }}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's put this in a decorator, though I'm not a fan of arbitrary wrappers for widths. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's really just to test it inside a container. By default it is width 100% |
||
<Select.Root {...args}> | ||
<Select.Trigger> | ||
<Select.Value placeholder="Select a fruit…" /> | ||
</Select.Trigger> | ||
<Select.Content> | ||
<Select.Item value="Avocado">Avocado</Select.Item> | ||
<Select.Item value="Banana">Banana</Select.Item> | ||
<Select.Item value="Bilberry">Bilberry</Select.Item> | ||
<Select.Item value="Blackberry">Blackberry</Select.Item> | ||
<Select.Item value="Blackcurrant">Blackcurrant</Select.Item> | ||
<Select.Item value="Black sapote">Black sapote</Select.Item> | ||
<Select.Item value="Blueberry">Blueberry</Select.Item> | ||
<Select.Item value="Boysenberry">Boysenberry</Select.Item> | ||
<Select.Item value="Breadfruit">Breadfruit</Select.Item> | ||
<Select.Item value="Cacao">Cacao</Select.Item> | ||
<Select.Item value="Cactus pear">Cactus pear</Select.Item> | ||
<Select.Item value="Canistel">Canistel</Select.Item> | ||
<Select.Item value="Catmon">Catmon</Select.Item> | ||
<Select.Item value="Cempedak">Cempedak</Select.Item> | ||
<Select.Item value="Cherimoya">Cherimoya</Select.Item> | ||
<Select.Item value="Cherry">Cherry</Select.Item> | ||
<Select.Item value="Chico fruit">Chico fruit</Select.Item> | ||
<Select.Item value="Cloudberry">Cloudberry</Select.Item> | ||
<Select.Item value="Coco de mer">Coco de mer</Select.Item> | ||
<Select.Item value="Coconut">Coconut</Select.Item> | ||
<Select.Item value="Crab apple">Crab apple</Select.Item> | ||
<Select.Item value="Cranberry">Cranberry</Select.Item> | ||
<Select.Item value="Currant">Currant</Select.Item> | ||
<Select.Item value="Damson">Damson</Select.Item> | ||
<Select.Item value="Date">Date</Select.Item> | ||
<Select.Item value="Dragonfruit">Dragonfruit</Select.Item> | ||
<Select.Item value="Durian">Durian</Select.Item> | ||
<Select.Item value="Elderberry">Elderberry</Select.Item> | ||
<Select.Item value="Feijoa">Feijoa</Select.Item> | ||
<Select.Item value="Fig">Fig</Select.Item> | ||
<Select.Item value="Finger Lime">Finger Lime</Select.Item> | ||
<Select.Item value="Gac Fruit">Gac Fruit</Select.Item> | ||
<Select.Item value="Goji berry">Goji berry</Select.Item> | ||
<Select.Item value="Gooseberry">Gooseberry</Select.Item> | ||
<Select.Item value="Grape">Grape</Select.Item> | ||
<Select.Item value="Raisin">Raisin</Select.Item> | ||
<Select.Item value="Grapefruit">Grapefruit</Select.Item> | ||
<Select.Item value="Grewia asiatica">Grewia asiatica</Select.Item> | ||
<Select.Item value="Guava">Guava</Select.Item> | ||
<Select.Item value="Guyabano">Guyabano</Select.Item> | ||
<Select.Item value="Hala Fruit">Hala Fruit</Select.Item> | ||
<Select.Item value="Honeyberry">Honeyberry</Select.Item> | ||
<Select.Item value="Huckleberry">Huckleberry</Select.Item> | ||
<Select.Item value="Jabuticaba">Jabuticaba</Select.Item> | ||
<Select.Item value="Jackfruit">Jackfruit</Select.Item> | ||
<Select.Item value="Jambul">Jambul</Select.Item> | ||
<Select.Item value="Japanese plum">Japanese plum</Select.Item> | ||
<Select.Item value="Jostaberry">Jostaberry</Select.Item> | ||
<Select.Item value="Jujube">Jujube</Select.Item> | ||
<Select.Item value="Juniper berry">Juniper berry</Select.Item> | ||
<Select.Item value="Kaffir Lime">Kaffir Lime</Select.Item> | ||
<Select.Item value="Kiwano">Kiwano</Select.Item> | ||
<Select.Item value="Kiwifruit">Kiwifruit</Select.Item> | ||
<Select.Item value="Kumquat">Kumquat</Select.Item> | ||
<Select.Item value="Lanzones">Lanzones</Select.Item> | ||
<Select.Item value="Lemon">Lemon</Select.Item> | ||
<Select.Item value="Lime">Lime</Select.Item> | ||
<Select.Item value="Loganberry">Loganberry</Select.Item> | ||
<Select.Item value="Longan">Longan</Select.Item> | ||
<Select.Item value="Loquat">Loquat</Select.Item> | ||
</Select.Content> | ||
</Select.Root> | ||
</div> | ||
), | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import * as React from 'react'; | ||
import * as SelectPrimitive from '@radix-ui/react-select'; | ||
import { styled } from '@storybook/theming'; | ||
import ExpandAlt from './icons/ExpandAlt'; | ||
import Arrowup from './icons/Arrowup'; | ||
import Arrowdown from './icons/Arrowdown'; | ||
import Check from './icons/Check'; | ||
|
||
const SelectTrigger = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Trigger>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> | ||
>(({ className, children, ...props }, ref) => ( | ||
<StyledTrigger ref={ref} {...props}> | ||
{children} | ||
<SelectPrimitive.Icon asChild> | ||
<ExpandAlt size={12} /> | ||
</SelectPrimitive.Icon> | ||
</StyledTrigger> | ||
)); | ||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; | ||
|
||
const SelectContent = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Content>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> | ||
>(({ className, children, ...props }, ref) => ( | ||
<SelectPrimitive.Portal> | ||
<StyledContent ref={ref} {...props}> | ||
<StyledScrollUpButton> | ||
<Arrowup size={12} /> | ||
</StyledScrollUpButton> | ||
<StyledViewport>{children}</StyledViewport> | ||
<StyledScrollDownButton> | ||
<Arrowdown size={12} /> | ||
</StyledScrollDownButton> | ||
</StyledContent> | ||
</SelectPrimitive.Portal> | ||
)); | ||
SelectContent.displayName = SelectPrimitive.Content.displayName; | ||
|
||
const SelectLabel = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Label>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> | ||
>(({ className, ...props }, ref) => <SelectPrimitive.Label ref={ref} {...props} />); | ||
SelectLabel.displayName = SelectPrimitive.Label.displayName; | ||
|
||
const SelectItem = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Item>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> | ||
>(({ className, children, ...props }, ref) => ( | ||
<StyledItem ref={ref} {...props}> | ||
<StyledItemIndicator> | ||
<Check size={12} /> | ||
</StyledItemIndicator> | ||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> | ||
</StyledItem> | ||
)); | ||
SelectItem.displayName = SelectPrimitive.Item.displayName; | ||
|
||
const SelectSeparator = React.forwardRef< | ||
React.ElementRef<typeof SelectPrimitive.Separator>, | ||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> | ||
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} {...props} />); | ||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName; | ||
|
||
export const Select = { | ||
Root: SelectPrimitive.Root, | ||
Group: SelectPrimitive.Group, | ||
Value: SelectPrimitive.Value, | ||
Trigger: SelectTrigger, | ||
Content: SelectContent, | ||
Label: SelectLabel, | ||
Item: SelectItem, | ||
Separator: SelectSeparator, | ||
}; | ||
Comment on lines
+65
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For complex combination-components like these That way when users import these, all they have to do to get a example snippet, is hover over this import. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just a wrapper on top of Radix with styles added to it. I can improve the docs to specify that I guess to make sure they go to Radix to understand all the use cases. We will not try to recreate their docs. |
||
|
||
const StyledTrigger = styled(SelectPrimitive.Trigger)(({ theme }) => ({ | ||
all: 'unset', | ||
display: 'flex', | ||
width: '100%', | ||
height: '32px', | ||
alignItems: 'center', | ||
justifyContent: 'space-between', | ||
transition: 'box-shadow 200ms ease-out, opacity 200ms ease-out', | ||
color: theme.input.color, | ||
background: theme.input.background, | ||
boxShadow: `${theme.input.border} 0 0 0 1px inset`, | ||
borderRadius: theme.input.borderRadius, | ||
fontSize: theme.typography.size.s2 - 1, | ||
padding: '6px 10px', | ||
boxSizing: 'border-box', | ||
lineHeight: '20px', | ||
|
||
'&:focus': { | ||
boxShadow: `${theme.color.secondary} 0 0 0 1px inset`, | ||
outline: 'none', | ||
}, | ||
|
||
'&[disabled]': { | ||
cursor: 'not-allowed', | ||
opacity: 0.5, | ||
}, | ||
|
||
'&[data-placeholder]': { | ||
color: theme.textMutedColor, | ||
}, | ||
|
||
'&:-webkit-autofill': { | ||
WebkitBoxShadow: `0 0 0 3em ${theme.color.lightest} inset`, | ||
}, | ||
})); | ||
|
||
const StyledContent = styled(SelectPrimitive.Content)(({ theme }) => ({ | ||
boxSizing: 'border-box', | ||
overflow: 'hidden', | ||
backgroundColor: theme.input.background, | ||
borderRadius: '6px', | ||
border: theme.base === 'dark' ? `1px solid ${theme.input.border}` : 'none', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting choice to have no border at all in dark-mode. Can you elaborate? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't have any layout shift since it will close the panel when you click anywhere else but you're right just to be safe we can add |
||
width: '100%', | ||
boxShadow: | ||
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', | ||
})); | ||
|
||
const StyledViewport = styled(SelectPrimitive.Viewport)(() => ({ | ||
boxSizing: 'border-box', | ||
width: '100%', | ||
padding: '5px', | ||
})); | ||
|
||
const StyledScrollUpButton = styled(SelectPrimitive.ScrollUpButton)(({ theme }) => ({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
height: '25px', | ||
backgroundColor: theme.input.background, | ||
color: theme.input.color, | ||
cursor: 'default', | ||
})); | ||
|
||
const StyledScrollDownButton = styled(SelectPrimitive.ScrollDownButton)(({ theme }) => ({ | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
height: '25px', | ||
backgroundColor: theme.input.background, | ||
color: theme.input.color, | ||
cursor: 'default', | ||
})); | ||
|
||
const StyledItem = styled(SelectPrimitive.Item)(({ theme }) => ({ | ||
fontSize: '13px', | ||
lineHeight: 1, | ||
color: theme.input.color, | ||
borderRadius: '3px', | ||
display: 'flex', | ||
alignItems: 'center', | ||
height: '25px', | ||
padding: '0 35px 0 25px', | ||
position: 'relative', | ||
userSelect: 'none', | ||
|
||
'&[data-disabled]': { | ||
color: 'red', | ||
pointerEvents: 'none', | ||
}, | ||
|
||
'&[data-highlighted]': { | ||
outline: 'none', | ||
backgroundColor: theme.barSelectedColor, | ||
color: theme.barBg, | ||
}, | ||
})); | ||
|
||
const StyledItemIndicator = styled(SelectPrimitive.ItemIndicator)(() => ({ | ||
position: 'absolute', | ||
left: 0, | ||
width: '25px', | ||
display: 'inline-flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding more diverse storybook examples to cover different use cases and edge cases. [medium]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting one. I could do that.