@hyphen/hyphen-components 9.0.0-beta.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "9.0.0-beta.1",
3
+ "version": "9.0.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -56,7 +56,7 @@
56
56
  "react-select": "^5.10.0",
57
57
  "react-select-event": "^5.5.1",
58
58
  "uuid": "^11.1.1",
59
- "@hyphen/hyphen-design-tokens": "^9.0.0-beta.1"
59
+ "@hyphen/hyphen-design-tokens": "^9.0.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@babel/core": "^7.27.4",
@@ -13,6 +13,9 @@ A `<Box>` is a layout component to build UIs with consistent padding and spacing
13
13
  - Allows you to specify the spacing between child elements using `gap`
14
14
  - Many props accept `ResponsiveProp` that take an object with keys that correspond to [breakpoint tokens](?path=/docs/foundation-design-tokens--docs#breakpoints).
15
15
 
16
+ See [Polymorphic Composition](?path=/docs/about-polymorphic-composition--docs)
17
+ for native element, router component, ref, and extracted prop object patterns.
18
+
16
19
  <Canvas of={Stories.Overview} />
17
20
 
18
21
  ## Props
@@ -75,4 +75,16 @@ The interface should make it clear why the button is disabled and what needs to
75
75
 
76
76
  ## As Child
77
77
 
78
- Apply the `asChild` prop to apply the button styles to a child element.
78
+ Apply the `asChild` prop to apply the button styles to one child element. This
79
+ keeps navigation semantics when a link needs to look like a button and prevents
80
+ nested interactive elements.
81
+
82
+ ```tsx
83
+ <Button asChild>
84
+ <a href="/settings">Settings</a>
85
+ </Button>
86
+ ```
87
+
88
+ Place navigation props on the child and Button props on `Button`. See
89
+ [Polymorphic Composition](?path=/docs/about-polymorphic-composition--docs) for
90
+ router links, custom ref-forwarding children, and accessibility guidance.
@@ -45,3 +45,10 @@ function App() {
45
45
 
46
46
  ReactDOM.render(<App />, document.querySelector('#app'));
47
47
  ```
48
+
49
+ ### 6. Compose with the right element
50
+
51
+ Use `as` or `asChild` when a Hyphen component needs to render with different
52
+ HTML semantics or reuse a router link or trigger. See
53
+ [Polymorphic Composition](?path=/docs/about-polymorphic-composition--docs) for
54
+ the supported patterns and accessibility guidance.
@@ -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.