Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
98 lines
4.2 KiB
Markdown
98 lines
4.2 KiB
Markdown
# Anchor Navigation
|
|
|
|
This document describes the pattern for navigating directly to a specific field within a page using URL hash fragments (e.g., `/account/accessibility-and-appearance#clockMode`).
|
|
|
|
## Problem
|
|
|
|
Rocket.Chat uses a custom SPA router that intercepts link clicks via `e.preventDefault()` and navigates with `history.pushState()`. This means the browser's native hash-based scrolling does not work for cross-page navigation. When a user clicks a link like `/page-b#field`, the router renders the new page but does not scroll to the target element.
|
|
|
|
## Solution
|
|
|
|
Scrolling to hash targets is handled automatically by `RouterProvider` via `useRouterScrollToHash`. Any element with an `id` matching the URL hash will be scrolled into view on navigation.
|
|
|
|
For pages that need to adjust layout based on the hash (e.g., expanding an accordion section), `useLocationHash()` from `@rocket.chat/ui-contexts` provides the current hash value.
|
|
|
|
### Key files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `apps/meteor/client/hooks/useRouterScrollToHash.ts` | Scroll logic, called by `RouterProvider` |
|
|
| `packages/ui-contexts/src/hooks/useLocationHash.ts` | Generic `useLocationHash()` hook from `@rocket.chat/ui-contexts` |
|
|
|
|
## How it works
|
|
|
|
1. A **source page** links to a target page with a hash fragment in the `href`
|
|
2. The **target page** renders elements with matching `id` attributes
|
|
3. `useRouterScrollToHash` (in `RouterProvider`) scrolls to the element matching the hash via `scrollIntoView`
|
|
4. If the target page needs to adjust layout (e.g., expand a collapsed section), it reads `useLocationHash()` to check if a hash is present
|
|
|
|
```text
|
|
PreferencesMessagesSection AccessibilityPage
|
|
┌──────────────────────────┐ ┌──────────────────────────┐
|
|
│ Message TimeFormat │ │ Adjustable layout │
|
|
│ [Go to accessibility] ────┼── #clockMode ───> │ <Field id="clockMode"> │
|
|
│ │ │ │
|
|
│ Hide usernames │ │ <Field id="hideUsernames"> │
|
|
│ [Go to accessibility] ────┼── #hideUsernames ───> │ │
|
|
└──────────────────────────┘ └──────────────────────────┘
|
|
```
|
|
|
|
## Usage
|
|
|
|
### 1. Add the `id` to the target element
|
|
|
|
On the destination page, set the `id` on the element you want to scroll to:
|
|
|
|
```tsx
|
|
<Field id='myNewField'>
|
|
<FieldLabel>{t('My_New_Field')}</FieldLabel>
|
|
{/* ... */}
|
|
</Field>
|
|
```
|
|
|
|
### 2. Expand collapsed sections (if needed)
|
|
|
|
If the target field is inside a collapsed section, use `useLocationHash()` to expand it:
|
|
|
|
```tsx
|
|
import { useLocationHash } from '@rocket.chat/ui-contexts';
|
|
|
|
const MyPage = () => {
|
|
const shouldExpand = useLocationHash().length > 1;
|
|
|
|
return (
|
|
<Accordion>
|
|
<AccordionItem defaultExpanded={shouldExpand} title={t('Section')}>
|
|
<Field id='myNewField'>
|
|
{/* ... */}
|
|
</Field>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 3. Link from the source page
|
|
|
|
Use the hash fragment in the `href`:
|
|
|
|
```tsx
|
|
<FieldLink href='/account/accessibility-and-appearance#myNewField'>
|
|
{t('Go_to_accessibility_and_appearance')}
|
|
</FieldLink>
|
|
```
|
|
|
|
## Why `scrollIntoView` is needed
|
|
|
|
The browser natively scrolls to hash targets on full page loads and un-intercepted anchor clicks. However, the custom router calls `e.preventDefault()` on cross-page links and uses `history.pushState()`, which by spec does not trigger scroll. `useRouterScrollToHash` compensates by calling `scrollIntoView` after each navigation.
|
|
|
|
Same-page hash navigation (clicking `#anchor` when already on that page) still works natively — the router detects matching pathnames and lets the browser handle it.
|
|
|
|
## Checklist
|
|
|
|
When adding a new anchor:
|
|
|
|
- [ ] `id` attribute set on the target element
|
|
- [ ] Source `href` includes the `#anchor` fragment
|
|
- [ ] `useLocationHash()` can be called in target page to identify if theres hash and control collapsed content, if layout adjustment is needed
|