@hyphen/hyphen-components 9.0.0-beta.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/utilities.css +1 -1
- package/dist/css/utilities.css.map +1 -1
- package/dist/hyphen-components.cjs.development.js +11 -2
- package/dist/hyphen-components.cjs.development.js.map +1 -1
- package/dist/hyphen-components.cjs.production.min.js +2 -2
- package/dist/hyphen-components.cjs.production.min.js.map +1 -1
- package/dist/hyphen-components.esm.js +11 -2
- package/dist/hyphen-components.esm.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/package.json +2 -2
- package/src/components/Box/Box.mdx +3 -0
- package/src/components/Button/Button.mdx +13 -1
- package/src/components/Table/Table.mdx +20 -0
- package/src/components/Table/Table.tsx +7 -7
- package/src/components/Table/TableBody/TableBody.test.tsx +89 -1
- package/src/components/Table/TableBody/TableBody.tsx +23 -8
- package/src/components/Table/TableHead/TableHead.tsx +6 -6
- package/src/components/Table/TableHead/TableHeaderCell/TableHeaderCell.tsx +6 -6
- package/src/components/Table/common/TableRow/TableRow.test.tsx +1 -1
- package/src/components/Table/common/TableRow/TableRow.tsx +11 -9
- package/src/docs/GetStarted.mdx +7 -0
- package/src/docs/PolymorphicComposition.mdx +184 -0
- package/src/lib/getColumnKeys.ts +5 -3
- package/src/types/index.ts +4 -4
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="About/Polymorphic Composition" />
|
|
4
|
+
|
|
5
|
+
# Polymorphic composition
|
|
6
|
+
|
|
7
|
+
Polymorphic composition changes the element a component renders without
|
|
8
|
+
giving up Hyphen styles. Use it to preserve the correct HTML semantics when a
|
|
9
|
+
component needs to act as a link, button, section, or router component.
|
|
10
|
+
|
|
11
|
+
Hyphen components expose two composition APIs:
|
|
12
|
+
|
|
13
|
+
| Goal | API | Use with |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Choose the element or component that Hyphen renders | `as` | `Box`, `Badge` |
|
|
16
|
+
| Apply Hyphen behavior and styles to an existing child | `asChild` | `Button` and compound component triggers or items that expose the prop |
|
|
17
|
+
|
|
18
|
+
## Render another element with `as`
|
|
19
|
+
|
|
20
|
+
`Box` renders a `div` by default. `Badge` does the same. Set `as` when another
|
|
21
|
+
element is more appropriate:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<Box as="main" padding="2xl">
|
|
25
|
+
<PageContent />
|
|
26
|
+
</Box>
|
|
27
|
+
|
|
28
|
+
<Box as="a" href="/docs" color="info">
|
|
29
|
+
Read the documentation
|
|
30
|
+
</Box>
|
|
31
|
+
|
|
32
|
+
<Badge as="a" href="/releases" color="info">
|
|
33
|
+
Beta
|
|
34
|
+
</Badge>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The target element determines the accepted DOM props and ref type. For
|
|
38
|
+
example, `href` is valid with `as="a"`, while `type` and `disabled` are valid
|
|
39
|
+
with `as="button"`:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useRef } from 'react';
|
|
43
|
+
|
|
44
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
45
|
+
|
|
46
|
+
<Box as="button" type="button" disabled ref={buttonRef}>
|
|
47
|
+
Retry
|
|
48
|
+
</Box>;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The element is never inferred from another prop. This is intentionally a type
|
|
52
|
+
error because the default element is still a `div`:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// Invalid: href does not turn the Box into an anchor.
|
|
56
|
+
<Box href="/docs">Read the documentation</Box>
|
|
57
|
+
|
|
58
|
+
// Valid: the element and its props agree.
|
|
59
|
+
<Box as="a" href="/docs">Read the documentation</Box>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Router and custom components
|
|
63
|
+
|
|
64
|
+
`as` also accepts React components. Put props for the rendered component on
|
|
65
|
+
the Hyphen component:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Link } from 'react-router-dom';
|
|
69
|
+
|
|
70
|
+
<Box as={Link} to="/settings" padding="sm md">
|
|
71
|
+
Settings
|
|
72
|
+
</Box>;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The rendered component must pass `className`, `style`, event handlers, and
|
|
76
|
+
other received props to its underlying element. It must also forward its ref
|
|
77
|
+
when the Hyphen component receives one.
|
|
78
|
+
|
|
79
|
+
## Compose an existing child with `asChild`
|
|
80
|
+
|
|
81
|
+
Use `asChild` when a component needs to provide behavior or styles while its
|
|
82
|
+
child owns the rendered element. This is the usual pattern for a link that
|
|
83
|
+
looks like a button:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Link } from 'react-router-dom';
|
|
87
|
+
|
|
88
|
+
<Button asChild variant="primary">
|
|
89
|
+
<Link to="/projects/new">Create project</Link>
|
|
90
|
+
</Button>;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Navigation props such as `to` and `href` belong on the child. Hyphen props
|
|
94
|
+
such as `variant`, `size`, and `iconPrefix` stay on `Button`.
|
|
95
|
+
|
|
96
|
+
The same pattern lets compound component triggers reuse an existing control
|
|
97
|
+
without adding another DOM element:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<Tooltip>
|
|
101
|
+
<TooltipTrigger asChild>
|
|
102
|
+
<Button variant="tertiary" aria-label="More information">
|
|
103
|
+
<Icon name="info" />
|
|
104
|
+
</Button>
|
|
105
|
+
</TooltipTrigger>
|
|
106
|
+
<TooltipContent>Deployment status</TooltipContent>
|
|
107
|
+
</Tooltip>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
An `asChild` component expects exactly one element that:
|
|
111
|
+
|
|
112
|
+
- accepts and forwards the injected props;
|
|
113
|
+
- merges the injected `className` instead of replacing it;
|
|
114
|
+
- forwards its ref to the underlying element;
|
|
115
|
+
- preserves the intended keyboard and accessibility behavior.
|
|
116
|
+
|
|
117
|
+
For a custom child, use `forwardRef` and spread the remaining props:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { ComponentPropsWithoutRef, forwardRef } from 'react';
|
|
121
|
+
|
|
122
|
+
type AppLinkProps = ComponentPropsWithoutRef<'a'> & {
|
|
123
|
+
trackingId: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const AppLink = forwardRef<HTMLAnchorElement, AppLinkProps>(
|
|
127
|
+
({ trackingId, ...props }, ref) => (
|
|
128
|
+
<a ref={ref} data-tracking-id={trackingId} {...props} />
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
<Button asChild>
|
|
133
|
+
<AppLink href="/docs" trackingId="docs-link">
|
|
134
|
+
Read the documentation
|
|
135
|
+
</AppLink>
|
|
136
|
+
</Button>;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Preserve semantic HTML
|
|
140
|
+
|
|
141
|
+
Choose the element based on behavior, not appearance:
|
|
142
|
+
|
|
143
|
+
- use a `button` for an action on the current page;
|
|
144
|
+
- use an anchor or router link for navigation;
|
|
145
|
+
- use landmarks such as `main`, `nav`, and `section` for page structure;
|
|
146
|
+
- do not nest interactive elements.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// Invalid: this creates an anchor inside a button.
|
|
150
|
+
<Button>
|
|
151
|
+
<a href="/settings">Settings</a>
|
|
152
|
+
</Button>
|
|
153
|
+
|
|
154
|
+
// Valid: one anchor receives the Button styles.
|
|
155
|
+
<Button asChild>
|
|
156
|
+
<a href="/settings">Settings</a>
|
|
157
|
+
</Button>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Native `disabled` behavior only exists on controls such as `button`. If a link
|
|
161
|
+
is unavailable, prefer not rendering it. If it must remain visible, use
|
|
162
|
+
`aria-disabled`, prevent navigation, and keep its focus behavior deliberate.
|
|
163
|
+
|
|
164
|
+
## Extract reusable prop objects
|
|
165
|
+
|
|
166
|
+
Use `satisfies` when moving polymorphic props into an object. It validates the
|
|
167
|
+
target element's props and preserves literal token values without adding
|
|
168
|
+
`as const` to every property:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import type { BoxProps } from '@hyphen/hyphen-components';
|
|
172
|
+
|
|
173
|
+
const documentationLinkProps = {
|
|
174
|
+
as: 'a',
|
|
175
|
+
href: '/docs',
|
|
176
|
+
color: 'info',
|
|
177
|
+
padding: 'sm md',
|
|
178
|
+
} satisfies BoxProps<'a'>;
|
|
179
|
+
|
|
180
|
+
<Box {...documentationLinkProps}>Read the documentation</Box>;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Use `BoxOwnProps` instead when the object contains only Hyphen style and
|
|
184
|
+
layout props and the caller will choose the rendered element.
|
package/src/lib/getColumnKeys.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Key } from 'react';
|
|
2
|
-
import { Column } from '../types';
|
|
2
|
+
import { Column, Row } from '../types';
|
|
3
3
|
|
|
4
4
|
// eslint-disable-next-line import/prefer-default-export
|
|
5
|
-
export const getColumnKeys =
|
|
5
|
+
export const getColumnKeys = <TRow extends object = Row>(
|
|
6
|
+
columns: Column<TRow>[]
|
|
7
|
+
): Key[] => {
|
|
6
8
|
const INTERNAL_KEY_PREFIX = 'columnKeyPrefix';
|
|
7
|
-
const columnKeys:
|
|
9
|
+
const columnKeys: Key[] = [];
|
|
8
10
|
const keys: Record<string, boolean> = {};
|
|
9
11
|
|
|
10
12
|
columns.forEach((column) => {
|
package/src/types/index.ts
CHANGED
|
@@ -149,7 +149,7 @@ export type Row = UnknownPropertiesObjType;
|
|
|
149
149
|
|
|
150
150
|
export type Cell = Row[string];
|
|
151
151
|
|
|
152
|
-
export declare type Column = {
|
|
152
|
+
export declare type Column<TRow extends object = Row> = {
|
|
153
153
|
/**
|
|
154
154
|
* Text alignment for column cells (including header alignment). Cells will default to left if not defined.
|
|
155
155
|
*/
|
|
@@ -159,11 +159,11 @@ export declare type Column = {
|
|
|
159
159
|
*/
|
|
160
160
|
cellClassName?:
|
|
161
161
|
| string
|
|
162
|
-
| ((cell?: Cell, row?:
|
|
162
|
+
| ((cell?: Cell, row?: TRow, rowIndex?: number) => string);
|
|
163
163
|
/**
|
|
164
164
|
* The key value to be rendered based on the table `rows`.
|
|
165
165
|
*/
|
|
166
|
-
dataKey?: string
|
|
166
|
+
dataKey?: Extract<keyof TRow, string>;
|
|
167
167
|
/**
|
|
168
168
|
* Placeholder for empty cells Applies only to the cells of the particular column with this prop.
|
|
169
169
|
*/
|
|
@@ -189,7 +189,7 @@ export declare type Column = {
|
|
|
189
189
|
* Render method for column cell data. Provides ability to render any aspect of the cell/row with custom
|
|
190
190
|
* markup.
|
|
191
191
|
*/
|
|
192
|
-
render?: (cell?: Cell, row?:
|
|
192
|
+
render?: (cell?: Cell, row?: TRow, rowIndex?: number) => ReactNode;
|
|
193
193
|
/**
|
|
194
194
|
* Whether the column is stuck to the left or right.
|
|
195
195
|
*/
|