|
| 1 | +import { Meta, Story, Controls } from '@storybook/addon-docs/blocks'; |
| 2 | +import { Link } from './Link'; |
| 3 | +import * as LinkStories from './Link.stories'; |
| 4 | + |
| 5 | +<Meta of={LinkStories} /> |
| 6 | + |
| 7 | +# Link |
| 8 | + |
| 9 | +Semantic navigation component styled as a textual link. It reuses `Button` under the hood with `type="link"`, but this page focuses on navigation via the `to` prop. |
| 10 | + |
| 11 | +## When to Use |
| 12 | + |
| 13 | +- Navigate inside your SPA using the configured router |
| 14 | +- Open destinations in a new tab |
| 15 | +- Jump to on‑page anchors via `#id` |
| 16 | +- Perform history navigation (back/forward/reload) using numbers |
| 17 | +- Bypass SPA routing and force a full page load |
| 18 | + |
| 19 | +## Component |
| 20 | + |
| 21 | +<Story of={LinkStories.Default} /> |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Properties |
| 26 | + |
| 27 | +<Controls /> |
| 28 | + |
| 29 | +### Base Properties |
| 30 | + |
| 31 | +Supports [Base properties](/docs/tasty-base-properties--docs) |
| 32 | + |
| 33 | +### Styling Properties |
| 34 | + |
| 35 | +#### styles |
| 36 | + |
| 37 | +Customizes the root element of the component. |
| 38 | + |
| 39 | +Sub-elements: |
| 40 | +- `ButtonIcon` – wrapper around `icon` and `rightIcon` when used. |
| 41 | + |
| 42 | +### Style Properties |
| 43 | + |
| 44 | +These properties allow direct styling without using the `styles` prop: `width`, `height`. |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Link Syntax (`to` prop) |
| 49 | + |
| 50 | +`to` defines where and how navigation happens. It accepts a string, an object path, or a number for history navigation. |
| 51 | + |
| 52 | +### 1) String URL/Path (SPA push) |
| 53 | + |
| 54 | +```jsx |
| 55 | +// Navigate to internal route via the navigation adapter |
| 56 | +<Link to="/pricing">Pricing</Link> |
| 57 | + |
| 58 | +// External absolute URLs always use native navigation |
| 59 | +<Link to="https://cube.dev">Cube</Link> |
| 60 | + |
| 61 | +// Relative paths work properly with router context |
| 62 | +<Link to="../parent-page">Parent</Link> |
| 63 | +``` |
| 64 | + |
| 65 | +### 2) Open in a new tab (`!` prefix) |
| 66 | + |
| 67 | +```jsx |
| 68 | +// Open internal route in a new tab |
| 69 | +<Link to="!/changelog">Changelog</Link> |
| 70 | + |
| 71 | +// Open external URL in a new tab |
| 72 | +<Link to="!https://cube.dev">Docs</Link> |
| 73 | +``` |
| 74 | + |
| 75 | +Notes: |
| 76 | +- Adds `target="_blank"` and `rel="noopener noreferrer"` automatically. |
| 77 | +- Cmd/Ctrl/Shift + click also opens in a new tab, like a native link. |
| 78 | + |
| 79 | +### 3) Native navigation / full reload (`@` prefix) |
| 80 | + |
| 81 | +```jsx |
| 82 | +// Bypass SPA router and perform a full page load |
| 83 | +<Link to="@/signin">Sign in</Link> |
| 84 | + |
| 85 | +// External native navigation works as well |
| 86 | +<Link to="@https://cube.dev">Homepage</Link> |
| 87 | +``` |
| 88 | + |
| 89 | +### 4) On‑page anchors (hash links) |
| 90 | + |
| 91 | +```jsx |
| 92 | +// Smoothly scrolls to the element with id="features" |
| 93 | +<Link to="#features">Jump to Features</Link> |
| 94 | +``` |
| 95 | + |
| 96 | +### 5) Object path (modern routers) |
| 97 | + |
| 98 | +```jsx |
| 99 | +// Uses navigate(to) with object path support |
| 100 | +<Link |
| 101 | + to={{ pathname: '/search', search: 'q=cube', hash: 'top' }} |
| 102 | +> |
| 103 | + Advanced search |
| 104 | +</Link> |
| 105 | +``` |
| 106 | + |
| 107 | +Accepted object shape: |
| 108 | +```ts |
| 109 | +{ pathname?: string; search?: string; hash?: string } |
| 110 | +``` |
| 111 | + |
| 112 | +Notes: |
| 113 | +- `search` and `hash` are auto-prefixed with `?` and `#` if needed |
| 114 | +- `pathname` can be relative (e.g., `../parent`) or absolute (e.g., `/root`) |
| 115 | +- Works with `!` and `@` prefixes when passed as strings |
| 116 | + |
| 117 | +### 6) History navigation (numbers) |
| 118 | + |
| 119 | +```jsx |
| 120 | +// Go back one entry |
| 121 | +<Link to={-1}>Back</Link> |
| 122 | + |
| 123 | +// Go forward one entry |
| 124 | +<Link to={1}>Forward</Link> |
| 125 | + |
| 126 | +// Reload current entry (like location.reload but via history) |
| 127 | +<Link to={0}>Reload</Link> |
| 128 | +``` |
| 129 | + |
| 130 | +Behavior: |
| 131 | +- Uses `navigate(delta)` from the navigation adapter |
| 132 | +- The default adapter calls `window.history.go(delta)` |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## Router Integration |
| 137 | + |
| 138 | +`Link` uses a hook-based navigation adapter that you provide via the `Root` component. The adapter enables proper relative path resolution for new-tab navigation and modern router integration. |
| 139 | + |
| 140 | +### Interface |
| 141 | + |
| 142 | +```ts |
| 143 | +interface NavigationAdapter { |
| 144 | + useHref: (to: To, opts?: { relative?: 'route' | 'path' }) => string; |
| 145 | + useNavigate: () => (to: To | number, opts?: NavigateOptions) => void; |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +### React Router v6+ (Recommended) |
| 150 | + |
| 151 | +React Router hooks are directly compatible with our `NavigationAdapter` interface: |
| 152 | + |
| 153 | +```jsx |
| 154 | +import { Root } from '@cube-dev/ui-kit'; |
| 155 | +import { useHref, useNavigate } from 'react-router-dom'; |
| 156 | + |
| 157 | +const navigation = { useHref, useNavigate }; |
| 158 | + |
| 159 | +export function App() { |
| 160 | + return ( |
| 161 | + <Root navigation={navigation}> |
| 162 | + {/* Your app content */} |
| 163 | + </Root> |
| 164 | + ); |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### Custom adapter example |
| 169 | + |
| 170 | +For other routers, create a custom adapter: |
| 171 | + |
| 172 | +```jsx |
| 173 | +import { Root } from '@cube-dev/ui-kit'; |
| 174 | + |
| 175 | +function MyRouterAdapter() { |
| 176 | + return { |
| 177 | + useHref: (to) => { |
| 178 | + // Return resolved href for the given route |
| 179 | + return typeof to === 'string' ? to : `${to.pathname || ''}${to.search || ''}${to.hash || ''}`; |
| 180 | + }, |
| 181 | + useNavigate: () => { |
| 182 | + return (to, options) => { |
| 183 | + if (typeof to === 'number') { |
| 184 | + window.history.go(to); |
| 185 | + } else { |
| 186 | + myRouter.navigate(to, options); |
| 187 | + } |
| 188 | + }; |
| 189 | + }, |
| 190 | + }; |
| 191 | +} |
| 192 | + |
| 193 | +export function App() { |
| 194 | + const navigation = MyRouterAdapter(); |
| 195 | + return <Root navigation={navigation}>{/* Your app */}</Root>; |
| 196 | +} |
| 197 | +``` |
| 198 | + |
| 199 | +### No router (default behavior) |
| 200 | + |
| 201 | +When no `navigation` prop is provided, `Link` falls back to native browser navigation: |
| 202 | + |
| 203 | +```jsx |
| 204 | +import { Root } from '@cube-dev/ui-kit'; |
| 205 | + |
| 206 | +export function App() { |
| 207 | + return ( |
| 208 | + <Root> |
| 209 | + {/* Links will use window.location for navigation */} |
| 210 | + </Root> |
| 211 | + ); |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +Default adapter behavior: |
| 216 | +- String/object navigation uses `window.location.assign(href)` |
| 217 | +- History navigation uses `window.history.go(delta)` |
| 218 | +- `!` prefix opens resolved URLs in a new tab |
| 219 | +- `@` prefix forces native navigation to resolved URLs |
| 220 | +- Relative paths are resolved against the current page |
| 221 | + |
| 222 | +--- |
| 223 | + |
| 224 | +## Navigation Options |
| 225 | + |
| 226 | +Control navigation behavior via the `navigationOptions` prop: |
| 227 | + |
| 228 | +```jsx |
| 229 | +<Link |
| 230 | + to="/search" |
| 231 | + navigationOptions={{ |
| 232 | + replace: true, |
| 233 | + state: { from: 'homepage' }, |
| 234 | + relative: 'path', |
| 235 | + preventScrollReset: true |
| 236 | + }} |
| 237 | +> |
| 238 | + Search (replace history) |
| 239 | +</Link> |
| 240 | +``` |
| 241 | + |
| 242 | +### Available options |
| 243 | + |
| 244 | +- `replace`: Replace current history entry instead of pushing a new one |
| 245 | +- `state`: Navigation state data (router-specific) |
| 246 | +- `relative`: How relative paths are resolved (`'route'` | `'path'`) |
| 247 | +- `preventScrollReset`: Prevent scroll reset on navigation |
| 248 | + |
| 249 | +These options are passed to the router's `navigate` function and have no effect on native navigation (`@` prefix, external URLs, or when no adapter is provided). |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## Variants |
| 254 | + |
| 255 | +`Link` inherits `theme` and `size` from `Button`. The visual `type` is fixed to `link`. |
| 256 | + |
| 257 | +### Themes |
| 258 | + |
| 259 | +- `default`, `danger`, `success`, `special` |
| 260 | + |
| 261 | +### Sizes |
| 262 | + |
| 263 | +- `xsmall`, `small`, `medium`, `large`, `xlarge` |
| 264 | + |
| 265 | +--- |
| 266 | + |
| 267 | +## Examples |
| 268 | + |
| 269 | +### Basic SPA push |
| 270 | + |
| 271 | +```jsx |
| 272 | +<Link to="/about">About</Link> |
| 273 | +``` |
| 274 | + |
| 275 | +### Anchor and new tab |
| 276 | + |
| 277 | +```jsx |
| 278 | +<Link to="#faq">FAQ</Link> |
| 279 | +<Link to="!https://cube.dev">Open Docs in new tab</Link> |
| 280 | +``` |
| 281 | + |
| 282 | +### Bypass SPA |
| 283 | + |
| 284 | +```jsx |
| 285 | +<Link to="@/logout">Log out</Link> |
| 286 | +``` |
| 287 | + |
| 288 | +### History |
| 289 | + |
| 290 | +```jsx |
| 291 | +<Link to={-1}>Back</Link> |
| 292 | +<Link to={0}>Reload</Link> |
| 293 | +``` |
| 294 | + |
| 295 | +--- |
| 296 | + |
| 297 | +## Accessibility |
| 298 | + |
| 299 | +- Renders as an `<a>` element when `to` is a string/object; as a `<button>` when `to` is a number or omitted. |
| 300 | +- Keyboard interaction and focus management are handled by React Aria. |
| 301 | +- Provide `label`/`aria-label` when there is no visible text. |
| 302 | + |
| 303 | +### ARIA Properties |
| 304 | + |
| 305 | +- `aria-label`, `aria-labelledby` – Accessible name. |
| 306 | +- Additional ARIA attributes supported by React Aria's `useButton`. |
| 307 | + |
| 308 | +--- |
| 309 | + |
| 310 | +## Best Practices |
| 311 | + |
| 312 | +1. Prefer SPA push (`to="/path"`) for in‑app navigation; use `@` only when a full reload is required. |
| 313 | +2. Use `!` for new tabs instead of adding `target` props yourself; security attributes are added automatically. |
| 314 | +3. Use object `to` when your router supports structured navigation. |
| 315 | +4. For back/forward actions in UI, use numeric `to` for predictable history behavior. |
| 316 | + |
| 317 | +## Suggested Improvements |
| 318 | + |
| 319 | +- Add a dedicated `href` prop alias for clarity in link‑only scenarios. |
| 320 | +- Expose a `replace` option for SPA navigation. |
| 321 | + |
| 322 | +## Related Components |
| 323 | + |
| 324 | +- [Button](/docs/actions-button--docs) – Action component with multiple visual styles, including `type="link"`. |
| 325 | + |
| 326 | + |
0 commit comments