Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exactActiveClass to A component #245

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default function App() {
}
```

The `<A>` tag also has an `active` class if its href matches the current location, and `inactive` otherwise. **Note:** By default matching includes locations that are descendents (eg. href `/users` matches locations `/users` and `/users/123`), use the boolean `end` prop to prevent matching these. This is particularly useful for links to the root route `/` which would match everything.
The `<A>` tag also has an `active` class if its href matches the current location and `inactive` class otherwise. By providing the property `exactActiveClass`, you can opt in to a third state, which is `exactActive` and is set when the href matches the current location exactly. **Note:** By default matching includes locations that are descendents (eg. href `/users` matches locations `/users` and `/users/123`). If no `exactActiveClass` property was provided, `active` class will be set for both partially and exactly matching routes.


| prop | type | description |
Expand All @@ -154,8 +154,9 @@ The `<A>` tag also has an `active` class if its href matches the current locatio
| replace | boolean | If true, don't add a new entry to the browser history. (By default, the new page will be added to the browser history, so pressing the back button will take you to the previous route.) |
| state | unknown | [Push this value](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to the history stack when navigating | |
| inactiveClass | string | The class to show when the link is inactive (when the current location doesn't match the link) |
| activeClass | string | The class to show when the link is active |
| end | boolean | If `true`, only considers the link to be active when the curent location matches the `href` exactly; if `false`, check if the current location _starts with_ `href` |
| activeClass | string | The class to show when the link is active, i.e. the current location _starts with_ `href` |
| exactActiveClass | string or true | The class to show when the link matches the `href` exactly. If `true`, applies `exactActive` class and enables strict matching - i.e. `activeClass` will not apply for an exact match.
| end | boolean | **Deprecated** If `true`, only considers the link to be active when the curent location matches the `href` exactly; if `false`, check if the current location _starts with_ `href` - providing `exactActiveClass` overrides this behavior | |

### The Navigate Component
Solid Router provides a `Navigate` component that works similarly to `A`, but it will _immediately_ navigate to the provided path as soon as the component is rendered. It also uses the `href` prop, but you have the additional option of passing a function to `href` that returns a path to navigate to:
Expand Down
25 changes: 19 additions & 6 deletions src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorEle
state?: unknown;
inactiveClass?: string;
activeClass?: string;
exactActiveClass?: true | string;
/**
* @deprecated end property deprecated in favor of 'exactActiveClass'
*/
end?: boolean;
}
export function A(props: AnchorProps) {
Expand All @@ -218,19 +222,27 @@ export function A(props: AnchorProps) {
"class",
"activeClass",
"inactiveClass",
"exactActiveClass",
"end"
]);
const to = useResolvedPath(() => props.href);
const href = useHref(to);
const location = useLocation();
const isActive = createMemo(() => {
const matchedHref = createMemo(() => {
const to_ = to();
if (to_ === undefined) return false;
if (to_ === undefined) return [false, false];
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
const loc = normalizePath(location.pathname).toLowerCase();
return props.end ? path === loc : loc.startsWith(path);
return [loc.startsWith(path), path === loc];
});

const isLooseMatch = createMemo(() => matchedHref()[0])
const isExactMatch = createMemo(() => matchedHref()[1] && Boolean(props.exactActiveClass))

// Remove together with `end` property
// If end was provided return an exact match, else return loose match (as long as users don't opt in for new behavior)
const isActiveDeprecated = createMemo(() => props.end ? matchedHref()[1] : !props.exactActiveClass && isLooseMatch())

return (
<a
link
Expand All @@ -239,11 +251,12 @@ export function A(props: AnchorProps) {
state={JSON.stringify(props.state)}
classList={{
...(props.class && { [props.class]: true }),
[props.inactiveClass!]: !isActive(),
[props.activeClass!]: isActive(),
[props.inactiveClass!]: !isLooseMatch(),
[props.activeClass!]: isLooseMatch() && !isExactMatch() || isActiveDeprecated(),
...(props.exactActiveClass && { [props.exactActiveClass === true ? 'exactActive' : props.exactActiveClass]: isExactMatch() }),
...rest.classList
}}
aria-current={isActive() ? "page" : undefined}
aria-current={isLooseMatch() ? "page" : undefined}
/>
);
}
Expand Down