28
loading...
This website collects cookies to deliver better user experience
// SomeComponent.style.tsx
export const StyledList = styled.dl``
export const StyledListItem = styled.div``
export const StyledListTitle = styled.dt``
export const StyledListContent = styled.dd``
// SomeComponent.tsx
function SomeComponent() {
return (
<StyledList>
<StyledListItem>
<StyledListTitle>Title</StyledListTitle>
<StyledListContent>Content</StyledListContent>
</StyledListItem>
</StyledList>
)
}
SomeComponent
itself you see no trace of <dl />
and the bunch! Sure, you can hover over the components and get type description which exposes that hey, it is a styled.dl
element. Or if you build a component library you can add documentation to a Storybook that tells how to use the components.as
property as a polymorphic feature. It allows you to tell which component does the rendering. Basically it is just this:function Polymorphic({ as: Component = 'div', ...props }) {
return <Component {...props />
}
// render as div
<Polymorphic>Hello</Polymorphic>
// render as button
<Polymorphic as="button">Hello</Polymorphic>
// render as some framework Link component
<Polymorphic as={Link}>Hello</Polymorphic>
<Button />
, <LinkButton />
, <TextLink />
, <TextLinkButton />
and whatever else. Although the issue in this particular example is that designers love to make visual links that have to act like buttons and visual buttons that have to act like links. But that is a completely another issue and has more to do with process.<FormControl element={<fieldset />}>
<FormTitle element={<legend />} />
</FormControl>
element
passed to element prop, and then the same thing again with the composing component.<Button element={<Link to="/" />}>
<HomeIcon />
Home
</Button>
Link
properties in the Button component! That is a very troublesome case in many frameworks that we currently have. Users of Next, Gatsby, or React Router are likely very familiar with the issue: the need of making your own additional special component wrapping an already specialized component.// here would be CSS actually
const StyledButton = styled.button``
interface ButtonProps {
element: JSX.Element
}
export function Button({ element }: ButtonProps) {
return <StyledButton as={element.type} {...element.props} />
}
element
props outside of our component entirely and we simply wrap a styled component to provide styles for the button. In this way the component itself becomes very focused and can do just what it needs to do, such as handle the styling concerns and additional functionality.button
, a link, or maybe even some hot garbage like a div
, and make it look like a button. But there is more! You can also fix the usability of any given component so you can apply ARIA attributes such as role="button"
and make sure all the accessibility guidelines are met (the ones we can safely do under-the-hood).element
is that it needs to support and pass through DOM attributes. If it doesn't, well, then we are doing work that never becomes effective. However our main goal here is to make HTML semantics visible so in that sense this is a non-issue.import styled from 'styled-components'
// CSS that assumes any element and making it look like a button
const StyledButton = styled.button``
const buttonTypes = new Set(['button', 'reset', 'submit'])
interface ButtonProps {
children?: React.ReactNode
element?: JSX.Element
}
function Button({ children, element }: ButtonProps) {
const { props } = element ?? <button />
// support `<button />` and `<input type={'button' | 'reset' | 'submit'} />` (or a custom button that uses `type` prop)
const isButton = element.type === 'button' || buttonTypes.has(props.type)
// it is really a link if it has `href` or `to` prop that has some content
const isLink = props.href != null || props.to != null
const { draggable = false, onDragStart, onKeyDown, role = 'button', tabIndex = 0, type } = props
const nextProps: React.HTMLProps<any> = React.useMemo(() => {
// make `<button />` default to `type="button"
if (isButton && type == null) {
return { type: 'button' }
}
if (!isButton && !isLink) {
return {
// default to not allowing dragging
draggable,
// prevent dragging the element in Firefox (match native `<button />` behavior)
onDragStart: onDragStart ?? ((event: React.DragEvent) => event.preventDefault()),
// Enter and Space must cause a click
onKeyDown: (event: React.KeyboardEvent<any>) => {
// consumer side handler is more important than we are
if (onKeyDown) onKeyDown(event)
// check that we are still allowed to do what we want to do
if (event.isDefaultPrevented() || !(event.target instanceof HTMLElement)) return
if ([' ', 'Enter'].includes(event.key)) {
event.target.click()
// let a possible third-party DOM listener know that somebody is already handling this event
event.preventDefault()
}
},
role,
tabIndex,
}
}
return null
}, [draggable, isButton, isLink, onDragStart, onKeyDown, role, tabIndex, type])
// ref may exist here but is not signaled in types, so hack it
const { ref } = (element as unknown) as { ref: any }
return (
<StyledButton as={element.type} ref={ref} {...props} {...nextProps}>
{children ?? props.children}
</StyledButton>
)
}
<button />
is type="submit"
if you don't let it know what it is. In my experience it is better to be explicit about type="submit"
.<Button>Button</Button>
// HTML:
<button class="..." type="button">Button</button>
<Button element={<button type="submit" />}>Submit button</Button>
// HTML:
<button class="..." type="submit">Submit button</button>
<Button element={<a href="#" />}>Link</Button>
// HTML:
<a class="..." href="#">Link</a>
<Button element={<a />}>Anchor</Button>
// HTML:
<a class="..." draggable="false" role="button" tabindex="0">Anchor</a>
<Button element={<div />}>Div</Button>
// HTML:
<div class="..." draggable="false" role="button" tabindex="0">Div</a>
<Button element={<Link to="#" />}>Link component</Button>
// HTML:
<a class="..." href="#">Link component</a>
onClick
is not a possibly mysterious click handler but you can be sure it is going to be a native click method. And the door is open for providing onClick
from the Button component that doesn't provide event
but instead something else!React.cloneElement
!return React.cloneElement(
element,
nextProps,
children ?? props.children
)
className
handling on your own.ref
we don't need to implement React.forwardRef
wrapper to our component. We also don't need to hack with the ref
variable like in the Styled Components implementation, because element
is passed to cloneElement
and does know about it. So that is one hackier side of code less in the implementation.