@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.
@@ -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.
@@ -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 = (columns: Column[]): Key[] => {
5
+ export const getColumnKeys = <TRow extends object = Row>(
6
+ columns: Column<TRow>[]
7
+ ): Key[] => {
6
8
  const INTERNAL_KEY_PREFIX = 'columnKeyPrefix';
7
- const columnKeys: React.Key[] = [];
9
+ const columnKeys: Key[] = [];
8
10
  const keys: Record<string, boolean> = {};
9
11
 
10
12
  columns.forEach((column) => {
@@ -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?: Row, rowIndex?: number) => string);
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?: Row, rowIndex?: number) => ReactNode;
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
  */