mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 01:02:02 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26612e371d | ||
|
|
b194506943 | ||
|
|
79dbffff1d | ||
|
|
0359934bb9 |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
.navds-accordion-anchor-menu {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.navds-accordion-anchor-menu__title {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
@@ -1,113 +1,85 @@
|
||||
/*******************
|
||||
* .accordion-menu *
|
||||
*******************/
|
||||
|
||||
.navds-accordion-menu__html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__nav {
|
||||
padding: var(--navds-layout-padding-small) 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.navds-accordion-menu__list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__title {
|
||||
margin: var(--navds-layout-padding-small) 0;
|
||||
padding: 0 var(--navds-layout-padding-small);
|
||||
}
|
||||
|
||||
.navds-accordion-menu__item {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: var(--navds-font-weight-bold);
|
||||
}
|
||||
|
||||
.navds-accordion-menu__item--active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0.5rem;
|
||||
height: 100%;
|
||||
background-color: var(--navds-color-blue-50);
|
||||
}
|
||||
|
||||
.navds-link.navds-accordion-menu__link,
|
||||
.navds-link.navds-accordion-menu__button {
|
||||
min-height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--navds-layout-padding-small);
|
||||
background: none;
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navds-link.navds-accordion-menu__link--active:not(:active):not(:focus) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--navds-color-text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__chevron {
|
||||
margin-right: 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__button {
|
||||
display: flex;
|
||||
clear: both;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font: inherit;
|
||||
text-align: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: var(--navds-font-weight-bold);
|
||||
color: var(--navds-color-text-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__dropdown--content .navds-accordion-menu__link,
|
||||
.navds-accordion-menu__dropdown--content .navds-accordion-menu__button {
|
||||
padding-left: calc(var(--navds-layout-padding-small) + 2rem);
|
||||
.navds-accordion-menu-collapsable .navds-accordion-menu__list,
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
font-weight: var(--navds-font-weight-regular);
|
||||
}
|
||||
|
||||
@media (min-width: 448px) {
|
||||
.navds-accordion-menu__nav {
|
||||
padding: var(--navds-layout-padding-medium-and-larger) 0;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__title {
|
||||
padding: 0 var(--navds-layout-padding-medium-and-larger);
|
||||
}
|
||||
|
||||
.navds-link.navds-accordion-menu__link,
|
||||
.navds-link.navds-accordion-menu__button {
|
||||
padding: 0 var(--navds-layout-padding-medium-and-larger);
|
||||
}
|
||||
|
||||
.navds-accordion-menu__dropdown--content .navds-accordion-menu__link,
|
||||
.navds-accordion-menu__dropdown--content .navds-accordion-menu__button {
|
||||
padding: 0 calc(var(--navds-layout-padding-medium-and-larger) + 2rem);
|
||||
}
|
||||
.navds-accordion-menu-item,
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
display: flex;
|
||||
padding: 0.75rem 1rem;
|
||||
line-height: 1.5rem; /* TODO: fix typography. */
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.navds-accordion-menu__nav {
|
||||
padding: var(--navds-layout-padding-small) 0;
|
||||
}
|
||||
.navds-accordion-menu-collapsable__expand-icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--navds-color-text-link);
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__title {
|
||||
padding: 0 var(--navds-layout-padding-small);
|
||||
}
|
||||
.navds-accordion-menu-collapsable--open
|
||||
> .navds-accordion-menu-collapsable__button
|
||||
> .navds-accordion-menu-collapsable__expand-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.navds-link.navds-accordion-menu__link,
|
||||
.navds-link.navds-accordion-menu__button {
|
||||
padding: 0 calc(var(--navds-layout-padding-small));
|
||||
}
|
||||
.navds-accordion-menu-collapsable .navds-accordion-menu-item,
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.navds-accordion-menu__dropdown--content .navds-accordion-menu__link,
|
||||
.navds-accordion-menu__dropdown--content .navds-accordion-menu__button {
|
||||
padding: 0 calc(var(--navds-layout-padding-small) + 1rem);
|
||||
}
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-item,
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.navds-accordion-menu-item:focus,
|
||||
.navds-accordion-menu-collapsable__button:focus {
|
||||
outline: 3px solid var(--navds-text-focus);
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.navds-accordion-menu-item:hover,
|
||||
.navds-accordion-menu-collapsable__button:hover {
|
||||
background-color: var(--navds-color-gray-20);
|
||||
}
|
||||
|
||||
.navds-accordion-menu-item--active {
|
||||
box-shadow: inset 0.5rem 0 var(--navds-color-text-link);
|
||||
color: var(--navds-color-text);
|
||||
text-decoration: none;
|
||||
font-weight: var(--navds-font-weight-bold);
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
@import "print.css";
|
||||
@import "utility.css";
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--navds-font-family);
|
||||
line-height: 1.375;
|
||||
@@ -16,3 +20,16 @@ body {
|
||||
a {
|
||||
color: var(--navds-color-text-link);
|
||||
}
|
||||
|
||||
/* https://web.dev/prefers-reduced-motion/ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, ::before, ::after {
|
||||
animation-delay: -1ms !important;
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
background-attachment: initial !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "baseline/baseline.css";
|
||||
@import "accordion.css";
|
||||
@import "accordion-anchor-menu.css";
|
||||
@import "accordion-menu.css";
|
||||
@import "alert.css";
|
||||
@import "button.css";
|
||||
|
||||
@@ -265,6 +265,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 448px) and (max-width: 959px) {
|
||||
.navds-layout__section .navds-accordion-anchor-menu__title {
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
.navds-layout__section .navds-accordion-menu-item,
|
||||
.navds-layout__section
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.navds-layout__section
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-item,
|
||||
.navds-layout__section
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
padding-left: 3.5rem;
|
||||
padding-right: 3.5rem;
|
||||
}
|
||||
|
||||
.navds-layout__section
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-item,
|
||||
.navds-layout__section
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable
|
||||
.navds-accordion-menu-collapsable__button {
|
||||
padding-left: 4.5rem;
|
||||
padding-right: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 648px) {
|
||||
/* prettier-ignore */
|
||||
.navds-layout__container--2-columns .navds-layout__panel--icon-margin:first-child {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.4",
|
||||
"private": false,
|
||||
"description": "Css for NAV Designsystem components",
|
||||
"author": "NAV Designsystem team",
|
||||
@@ -21,7 +21,7 @@
|
||||
"watch-css": "postcss -w --use postcss-import -o dist/index.css index.css"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^0.1.1",
|
||||
"@navikt/ds-tokens": "^0.1.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"postcss": "^8.2.7",
|
||||
"postcss-cli": "^8.3.1",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { forwardRef, HTMLAttributes } from "react";
|
||||
import { AccordionMenu as BaseAccordionMenu } from "../accordion-menu";
|
||||
import { Heading } from "../index";
|
||||
import { ActiveAnchorProvider } from "./ActiveAnchorStore";
|
||||
import cl from "classnames";
|
||||
|
||||
export interface AccordionAnchorMenuProps
|
||||
extends HTMLAttributes<HTMLDivElement> {
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionAnchorMenu = forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionAnchorMenuProps
|
||||
>(({ children, title, className, ...rest }, ref) => (
|
||||
<div
|
||||
className={cl("navds-accordion-anchor-menu", className)}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
{title && (
|
||||
<Heading
|
||||
level={2}
|
||||
size="medium"
|
||||
className="navds-accordion-anchor-menu__title"
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
)}
|
||||
<ActiveAnchorProvider>
|
||||
<BaseAccordionMenu aria-label={title || "Meny"}>
|
||||
{children}
|
||||
</BaseAccordionMenu>
|
||||
</ActiveAnchorProvider>
|
||||
</div>
|
||||
));
|
||||
|
||||
export default AccordionAnchorMenu;
|
||||
@@ -0,0 +1 @@
|
||||
export { AccordionMenuCollapsable as AccordionAnchorMenuCollapsable } from "../accordion-menu";
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { forwardRef, useEffect } from "react";
|
||||
import { AccordionMenuItem, AccordionMenuItemType } from "../accordion-menu";
|
||||
import { useStore } from "./ActiveAnchorStore";
|
||||
|
||||
const AccordionAnchorMenuItem: AccordionMenuItemType = forwardRef(
|
||||
(props, ref) => {
|
||||
const isAnchorActive = useIsAnchorActive(
|
||||
props.href && props.href.split("#")[1]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionMenuItem
|
||||
{...props}
|
||||
ref={ref}
|
||||
active={props.active || isAnchorActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const useIsAnchorActive = (anchor: string | undefined) => {
|
||||
const { activeAnchor, registerAnchor, unregisterAnchor } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor) {
|
||||
const target = document.getElementById(anchor);
|
||||
if (target) {
|
||||
registerAnchor(anchor);
|
||||
return () => {
|
||||
unregisterAnchor(anchor);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [anchor, registerAnchor, unregisterAnchor]);
|
||||
|
||||
return (anchor && activeAnchor === anchor) || false;
|
||||
};
|
||||
|
||||
export default AccordionAnchorMenuItem;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
|
||||
export interface ActiveAnchorStore {
|
||||
activeAnchor?: string;
|
||||
registerAnchor: (anchor: string) => void;
|
||||
unregisterAnchor: (anchor: string) => void;
|
||||
}
|
||||
|
||||
export const ActiveAnchorContext = createContext({} as ActiveAnchorStore);
|
||||
export const ActiveAnchorProvider = ({ children }) => {
|
||||
const [anchors, setAnchors] = useState<string[]>([]);
|
||||
const [activeAnchor, setActiveAnchor] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const scrollListener = () => {
|
||||
const offset = 100;
|
||||
const lastPassedAnchor = anchors
|
||||
.map((anchor) => document.getElementById(anchor))
|
||||
.map((element: HTMLElement) => ({
|
||||
id: element.id,
|
||||
top: element.getBoundingClientRect().top - offset,
|
||||
scrolledToBottom:
|
||||
window.innerHeight + window.pageYOffset >=
|
||||
document.body.offsetHeight,
|
||||
}))
|
||||
.filter((element) => element.scrolledToBottom || element.top <= 0)
|
||||
.sort((a, b) => (a.top < b.top ? -1 : 1))
|
||||
.map((anchor) => anchor.id)
|
||||
.pop();
|
||||
|
||||
// Set active anchor and related url hash
|
||||
if (lastPassedAnchor && activeAnchor !== lastPassedAnchor) {
|
||||
const { href, hash } = window.location;
|
||||
const urlWithoutHash = href.replace(hash, "");
|
||||
const urlWithAnchor = `${urlWithoutHash}#${lastPassedAnchor}`;
|
||||
const title = document.title;
|
||||
window.history.pushState(lastPassedAnchor, title, urlWithAnchor);
|
||||
setActiveAnchor(lastPassedAnchor);
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", scrollListener);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", scrollListener);
|
||||
};
|
||||
}, [anchors, activeAnchor]);
|
||||
|
||||
return (
|
||||
<ActiveAnchorContext.Provider
|
||||
value={{
|
||||
activeAnchor,
|
||||
registerAnchor: useCallback((anchor) => {
|
||||
setAnchors((anchors) => [...anchors, anchor]);
|
||||
}, []),
|
||||
unregisterAnchor: useCallback((anchor) => {
|
||||
setAnchors((anchors) => anchors.filter((a) => a !== anchor));
|
||||
}, []),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActiveAnchorContext.Provider>
|
||||
);
|
||||
};
|
||||
export const useStore = () => useContext(ActiveAnchorContext);
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as AccordionAnchorMenu } from "./AccordionAnchorMenu";
|
||||
export { AccordionAnchorMenuCollapsable } from "./AccordionAnchorMenuCollapsable";
|
||||
export { default as AccordionAnchorMenuItem } from "./AccordionAnchorMenuItem";
|
||||
+29
-27
@@ -1,5 +1,9 @@
|
||||
import * as React from "react";
|
||||
import Menu from "../index";
|
||||
import {
|
||||
AccordionAnchorMenu as Menu,
|
||||
AccordionAnchorMenuCollapsable as Collapsable,
|
||||
AccordionAnchorMenuItem as Item,
|
||||
} from "../index";
|
||||
import { useEffect } from "react";
|
||||
import { ProductPageLayout } from "../../index";
|
||||
import { Right } from "../../layouts/stories/components/sections/Right";
|
||||
@@ -9,10 +13,8 @@ import { MainTwo } from "../../layouts/stories/components/sections/Main";
|
||||
import { MainThree } from "../../layouts/stories/components/sections/Main";
|
||||
import { Link, HashRouter as Router } from "react-router-dom";
|
||||
|
||||
import "./components/styles.css";
|
||||
|
||||
export default {
|
||||
title: "ds-react/accordion-menu",
|
||||
title: "ds-react/accordion-anchor-menu",
|
||||
component: { Menu },
|
||||
decorators: [
|
||||
(Story) => {
|
||||
@@ -35,13 +37,13 @@ export default {
|
||||
export const All = () => (
|
||||
<ProductPageLayout title={"Nunc et lorem"}>
|
||||
<ProductPageLayout.Section left sticky withPadding={false}>
|
||||
<Menu title={"Minima"}>
|
||||
<Menu.Item href={"#leo-quis"}>Leo quis</Menu.Item>
|
||||
<Menu.Collapsable title={"Proin accumsan"}>
|
||||
<Menu.Item href={"#nulla-pariatur"}>Nulla pariatur</Menu.Item>
|
||||
<Menu.Item href={"#luctus-justo"}>Luctus justo</Menu.Item>
|
||||
</Menu.Collapsable>
|
||||
<Menu.Item href={"#maecenas-in-pretium"}>Maecenas in pretium</Menu.Item>
|
||||
<Menu title={"Minima"} smoothScrollBehavior>
|
||||
<Item href={"#leo-quis"}>Leo quis</Item>
|
||||
<Collapsable title={"Proin accumsan"}>
|
||||
<Item href={"#nulla-pariatur"}>Nulla pariatur</Item>
|
||||
<Item href={"#luctus-justo"}>Luctus justo</Item>
|
||||
</Collapsable>
|
||||
<Item href={"#maecenas-in-pretium"}>Maecenas in pretium</Item>
|
||||
</Menu>
|
||||
</ProductPageLayout.Section>
|
||||
<ProductPageLayout.Section whiteBackground={false} withPadding={false}>
|
||||
@@ -77,28 +79,28 @@ export const ReactRouter = () => (
|
||||
<ProductPageLayout title={"Nunc et lorem"}>
|
||||
<ProductPageLayout.Section left sticky withPadding={false}>
|
||||
<Menu>
|
||||
<Menu.Item component={Link} to="/link1">
|
||||
<Item component={Link} to="/link1">
|
||||
Leo quis
|
||||
</Menu.Item>
|
||||
<Menu.Collapsable title={"Proin accumsan"}>
|
||||
<Menu.Item component={Link} to="/link2">
|
||||
</Item>
|
||||
<Collapsable title={"Proin accumsan"}>
|
||||
<Item component={Link} to="/link2">
|
||||
Nulla pariatur
|
||||
</Menu.Item>
|
||||
<Menu.Item component={Link} to="/link3">
|
||||
</Item>
|
||||
<Item component={Link} to="/link3">
|
||||
Luctus justo
|
||||
</Menu.Item>
|
||||
</Menu.Collapsable>
|
||||
<Menu.Collapsable title={"Sint cupidatat"}>
|
||||
<Menu.Item component={Link} to="/link4">
|
||||
</Item>
|
||||
</Collapsable>
|
||||
<Collapsable title={"Sint cupidatat"}>
|
||||
<Item component={Link} to="/link4">
|
||||
Nulla pariatur
|
||||
</Menu.Item>
|
||||
<Menu.Item component={Link} to="/link5">
|
||||
</Item>
|
||||
<Item component={Link} to="/link5">
|
||||
Luctus justo
|
||||
</Menu.Item>
|
||||
</Menu.Collapsable>
|
||||
<Menu.Item component={Link} to="/link6">
|
||||
</Item>
|
||||
</Collapsable>
|
||||
<Item component={Link} to="/link6">
|
||||
Maecenas in pretium
|
||||
</Menu.Item>
|
||||
</Item>
|
||||
</Menu>
|
||||
</ProductPageLayout.Section>
|
||||
<ProductPageLayout.Section whiteBackground={false} withPadding={false}>
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { forwardRef, HTMLAttributes } from "react";
|
||||
import cl from "classnames";
|
||||
import MenuItems from "./MenuItems";
|
||||
|
||||
export interface AccordionMenuProps extends HTMLAttributes<HTMLElement> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionMenu = forwardRef<HTMLElement, AccordionMenuProps>(
|
||||
({ children, className, ...rest }, ref) => {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
ref={ref}
|
||||
className={cl("navds-accordion-menu", className)}
|
||||
{...rest}
|
||||
>
|
||||
<MenuItems>{children}</MenuItems>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default AccordionMenu;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { forwardRef, HTMLAttributes, useState } from "react";
|
||||
import cl from "classnames";
|
||||
import { Expand } from "@navikt/ds-icons";
|
||||
import MenuItems from "./MenuItems";
|
||||
|
||||
export interface AccordionMenuCollapsableProps
|
||||
extends HTMLAttributes<HTMLDivElement> {
|
||||
title: string;
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionMenuCollapsable = forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionMenuCollapsableProps
|
||||
>(({ children, defaultOpen = false, title, className, ...rest }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cl("navds-accordion-menu-collapsable", className, {
|
||||
"navds-accordion-menu-collapsable--open": isOpen,
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="navds-accordion-menu-collapsable__button"
|
||||
>
|
||||
{title}
|
||||
<Expand
|
||||
role="img"
|
||||
aria-label={isOpen ? "Pil peker opp" : "Pil peker ned"}
|
||||
className="navds-accordion-menu-collapsable__expand-icon"
|
||||
/>
|
||||
</button>
|
||||
{isOpen && <MenuItems>{children}</MenuItems>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default AccordionMenuCollapsable;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { OverridableComponent } from "../util";
|
||||
import cl from "classnames";
|
||||
|
||||
export type AccordionMenuItemType = OverridableComponent<AccordionMenuItemProps>;
|
||||
|
||||
export interface AccordionMenuItemProps {
|
||||
props: {
|
||||
children: React.ReactNode;
|
||||
active?: boolean;
|
||||
} & React.HTMLAttributes<HTMLAnchorElement>;
|
||||
defaultComponent: "a";
|
||||
}
|
||||
|
||||
const AccordionMenuItem: AccordionMenuItemType = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
component: Component = "a",
|
||||
active = false,
|
||||
className,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={cl("navds-accordion-menu-item", className, {
|
||||
"navds-accordion-menu-item--active": active,
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default AccordionMenuItem;
|
||||
@@ -1,40 +0,0 @@
|
||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
interface Props {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface ActiveAnchorStore {
|
||||
anchors: string[];
|
||||
activeAnchor?: string;
|
||||
registerAnchor: (anchor: string) => void;
|
||||
unregisterAnchor: (anchor: string) => void;
|
||||
setActiveAnchor: (anchor: string) => void;
|
||||
}
|
||||
|
||||
export const ActiveAnchorContext = createContext({} as ActiveAnchorStore);
|
||||
export const ActiveAnchorProvider = (props: Props) => {
|
||||
const { children } = props;
|
||||
const [anchors, setAnchors] = useState<string[]>([]);
|
||||
const [activeAnchor, setActiveAnchor] = useState<string>();
|
||||
|
||||
return (
|
||||
<ActiveAnchorContext.Provider
|
||||
value={{
|
||||
anchors,
|
||||
activeAnchor,
|
||||
registerAnchor: useCallback((anchor) => {
|
||||
setAnchors((anchors) => [...anchors, anchor]);
|
||||
}, []),
|
||||
unregisterAnchor: useCallback((anchor) => {
|
||||
setAnchors((anchors) => anchors.filter((a) => a !== anchor));
|
||||
}, []),
|
||||
setActiveAnchor: useCallback((anchor) => {
|
||||
setActiveAnchor(anchor);
|
||||
}, []),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActiveAnchorContext.Provider>
|
||||
);
|
||||
};
|
||||
export const useStore = () => useContext(ActiveAnchorContext);
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, { forwardRef, HTMLAttributes, useState } from "react";
|
||||
import cl from "classnames";
|
||||
import { Expand } from "@navikt/ds-icons";
|
||||
|
||||
export interface AccordionMenuCollapsableProps
|
||||
extends HTMLAttributes<HTMLButtonElement> {
|
||||
title: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const Collapsable = forwardRef<
|
||||
HTMLButtonElement,
|
||||
AccordionMenuCollapsableProps
|
||||
>(({ children, active, title, className, ...rest }, ref) => {
|
||||
const [open, setOpen] = useState(active || false);
|
||||
return (
|
||||
<li
|
||||
className={cl(
|
||||
"navds-accordion-menu__item",
|
||||
"navds-accordion-menu__dropdown"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={() => setOpen(!open)}
|
||||
className={cl("navds-link", "navds-accordion-menu__button", className)}
|
||||
{...rest}
|
||||
>
|
||||
{title}
|
||||
<Expand
|
||||
className={cl(
|
||||
"navds-accordion__chevron",
|
||||
`navds-accordion__chevron--${open ? "up" : "down"}`,
|
||||
`navds-accordion-menu__chevron`
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<ul
|
||||
className={cl(
|
||||
"navds-accordion-menu__container",
|
||||
"navds-accordion-menu__dropdown--content"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
export default Collapsable;
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { forwardRef, useEffect } from "react";
|
||||
import { useStore } from "./ActiveAnchorStore";
|
||||
import { OverridableComponent } from "../util";
|
||||
import cl from "classnames";
|
||||
|
||||
export interface AccordionMenuItemProps {
|
||||
props: {
|
||||
children: React.ReactNode;
|
||||
active?: boolean;
|
||||
} & React.HTMLAttributes<HTMLLIElement>;
|
||||
defaultComponent: "a";
|
||||
}
|
||||
|
||||
const Item: OverridableComponent<AccordionMenuItemProps> = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
component: Component = "a",
|
||||
active = false,
|
||||
className,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const anchor = rest.href && rest.href.split("#")[1];
|
||||
const { activeAnchor, registerAnchor, unregisterAnchor } = useStore();
|
||||
const isActive = active || (anchor && activeAnchor === anchor) || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor) {
|
||||
const target = document.getElementById(anchor);
|
||||
if (target) {
|
||||
registerAnchor(anchor);
|
||||
return () => {
|
||||
unregisterAnchor(anchor);
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [anchor, registerAnchor, unregisterAnchor]);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cl(
|
||||
"navds-accordion-menu__item",
|
||||
isActive && "navds-accordion-menu__item--active"
|
||||
)}
|
||||
>
|
||||
<Component
|
||||
ref={ref}
|
||||
className={cl("navds-link", "navds-accordion-menu__link", className, {
|
||||
"navds-accordion-menu__link--active": isActive,
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Item;
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
ForwardRefExoticComponent,
|
||||
HTMLAttributes,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { default as Item, AccordionMenuItemProps } from "./Item";
|
||||
import { default as Collapsable } from "./Collapsable";
|
||||
import { AccordionMenuCollapsableProps } from "./Collapsable";
|
||||
import { Heading, OverridableComponent } from "../index";
|
||||
import { ActiveAnchorProvider } from "./ActiveAnchorStore";
|
||||
import Nav from "./Nav";
|
||||
import cl from "classnames";
|
||||
|
||||
export interface LayoutWithSubComponents
|
||||
extends ForwardRefExoticComponent<AccordionMenuProps> {
|
||||
Collapsable: ForwardRefExoticComponent<AccordionMenuCollapsableProps>;
|
||||
Item: OverridableComponent<AccordionMenuItemProps>;
|
||||
}
|
||||
|
||||
export interface AccordionMenuProps extends HTMLAttributes<HTMLUListElement> {
|
||||
title?: string;
|
||||
smoothScrollBehavior?: boolean;
|
||||
}
|
||||
|
||||
const AccordionMenu = forwardRef<HTMLUListElement, AccordionMenuProps>(
|
||||
(
|
||||
{ children, title, smoothScrollBehavior = true, className, ...rest },
|
||||
ref
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (smoothScrollBehavior) {
|
||||
const htmlElement = document.documentElement;
|
||||
htmlElement.classList.add("navds-accordion-menu__html");
|
||||
return () => {
|
||||
htmlElement.classList.remove("navds-accordion-menu__html");
|
||||
};
|
||||
}
|
||||
}, [smoothScrollBehavior]);
|
||||
|
||||
return (
|
||||
<ActiveAnchorProvider>
|
||||
<Nav title={title}>
|
||||
{title && (
|
||||
<Heading
|
||||
level={2}
|
||||
size="medium"
|
||||
className={cl("navds-accordion-menu__title", className)}
|
||||
>
|
||||
{title}
|
||||
</Heading>
|
||||
)}
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cl("navds-accordion-menu__container", className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
</Nav>
|
||||
</ActiveAnchorProvider>
|
||||
);
|
||||
}
|
||||
) as LayoutWithSubComponents;
|
||||
|
||||
AccordionMenu.Item = Item;
|
||||
AccordionMenu.Collapsable = Collapsable;
|
||||
export default AccordionMenu;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
const MenuItems = ({ children }) => (
|
||||
<ul className="navds-accordion-menu__list">
|
||||
{React.Children.toArray(children).map((child, i) => (
|
||||
<li key={i} className="navds-accordion-menu__list-item">
|
||||
{child}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
export default MenuItems;
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { forwardRef, HTMLAttributes, useEffect } from "react";
|
||||
import { useStore } from "./ActiveAnchorStore";
|
||||
|
||||
export interface AccordionMenuItemProps
|
||||
extends HTMLAttributes<HTMLAnchorElement> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Nav = forwardRef<HTMLAnchorElement, AccordionMenuItemProps>(
|
||||
({ children, title, className, ...rest }, ref) => {
|
||||
const { anchors, activeAnchor, setActiveAnchor } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const scrollListener = () => {
|
||||
const offset = 100;
|
||||
const lastPassedAnchor = anchors
|
||||
.map((anchor) => document.getElementById(anchor))
|
||||
.map((element: HTMLElement) => ({
|
||||
id: element.id,
|
||||
top: element.getBoundingClientRect().top - offset,
|
||||
scrolledToBottom:
|
||||
window.innerHeight + window.pageYOffset >=
|
||||
document.body.offsetHeight,
|
||||
}))
|
||||
.filter((element) => element.scrolledToBottom || element.top <= 0)
|
||||
.sort((a, b) => (a.top < b.top ? -1 : 1))
|
||||
.map((anchor) => anchor.id)
|
||||
.pop();
|
||||
|
||||
// Set active anchor and related url hash
|
||||
if (lastPassedAnchor && activeAnchor !== lastPassedAnchor) {
|
||||
const { href, hash } = window.location;
|
||||
const urlWithoutHash = href.replace(hash, "");
|
||||
const urlWithAnchor = `${urlWithoutHash}#${lastPassedAnchor}`;
|
||||
const title = document.title;
|
||||
window.history.pushState(lastPassedAnchor, title, urlWithAnchor);
|
||||
setActiveAnchor(lastPassedAnchor);
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", scrollListener);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", scrollListener);
|
||||
};
|
||||
}, [anchors, activeAnchor, setActiveAnchor]);
|
||||
|
||||
return (
|
||||
<nav className={"navds-accordion-menu__nav"} aria-label={title || "Meny"}>
|
||||
{children}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Nav;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { default as AccordionMenu, AccordionMenuProps } from "./Menu";
|
||||
import { AccordionMenuItemProps } from "./Item";
|
||||
|
||||
export { AccordionMenu, AccordionMenuProps, AccordionMenuItemProps };
|
||||
export default AccordionMenu;
|
||||
export { default as AccordionMenu } from "./AccordionMenu";
|
||||
export { default as AccordionMenuCollapsable } from "./AccordionMenuCollapsable";
|
||||
export {
|
||||
default as AccordionMenuItem,
|
||||
AccordionMenuItemType,
|
||||
} from "./AccordionMenuItem";
|
||||
@@ -0,0 +1,160 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
AccordionMenu,
|
||||
AccordionMenuItem as Item,
|
||||
AccordionMenuCollapsable as Collapsable,
|
||||
} from "../index";
|
||||
import { NavLink, HashRouter as Router } from "react-router-dom";
|
||||
|
||||
export default {
|
||||
title: "ds-react/accordion-menu",
|
||||
component: { AccordionMenu },
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: "light",
|
||||
values: [
|
||||
{
|
||||
name: "light",
|
||||
value: "#F1F1F1",
|
||||
},
|
||||
{
|
||||
name: "white",
|
||||
value: "#fff",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const All = () => (
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
maxWidth: 288,
|
||||
}}
|
||||
>
|
||||
<AccordionMenu>
|
||||
<Item href="#leo">Leo</Item>
|
||||
<Collapsable title="Proin">
|
||||
<Item href="#nulla" active>
|
||||
Nulla
|
||||
</Item>
|
||||
<Item href="#luctus">Luctus</Item>
|
||||
</Collapsable>
|
||||
<Collapsable title="Accumsan">
|
||||
<Item href="#justo" ariaCurrent="page">
|
||||
Justo
|
||||
</Item>
|
||||
<Item href="#pariatur" active ariaCurrent="page">
|
||||
Pariatur
|
||||
</Item>
|
||||
<Collapsable title="Proin">
|
||||
<Item href="#nulla" active>
|
||||
Nulla
|
||||
</Item>
|
||||
<Item href="#luctus">Luctus</Item>
|
||||
</Collapsable>
|
||||
</Collapsable>
|
||||
<Item href="#justo" active>
|
||||
Justo
|
||||
</Item>
|
||||
<Item href="#pariatur">Pariatur</Item>
|
||||
</AccordionMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ReactRouter = () => (
|
||||
<AccordionMenu>
|
||||
<Collapsable title="Nivå 1 – 3 rems høy">
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/1"
|
||||
>
|
||||
Nivå 2 innrykk 2 rems
|
||||
</Item>
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/2"
|
||||
>
|
||||
Subtitle 3
|
||||
</Item>
|
||||
</Collapsable>
|
||||
<Collapsable title="Skjema og søknad">
|
||||
<Collapsable title="Satser">
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/3"
|
||||
>
|
||||
Nivå 3 – 3 rems innrykk Har du rett til dagpenger når du mottar annen
|
||||
økonomisk støtte fra NAV?
|
||||
</Item>
|
||||
</Collapsable>
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/4"
|
||||
>
|
||||
Subtitle 2
|
||||
</Item>
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/5"
|
||||
>
|
||||
Subtitle 3
|
||||
</Item>
|
||||
</Collapsable>
|
||||
<Collapsable title="Sakbehandlingstider">
|
||||
<Collapsable title="Subtitle 4 Lang tittel på andre nivå lorem ipsum">
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/6"
|
||||
>
|
||||
Subtitle 5
|
||||
</Item>
|
||||
</Collapsable>
|
||||
</Collapsable>
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/7"
|
||||
>
|
||||
Internasjonalt
|
||||
</Item>
|
||||
<Collapsable title="Meld fra om endringer lang tittel på første nivå">
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/8"
|
||||
>
|
||||
Subtitle 6
|
||||
</Item>
|
||||
</Collapsable>
|
||||
<Item
|
||||
component={NavLink}
|
||||
activeClassName="navds-accordion-menu-item--active"
|
||||
to="/9"
|
||||
>
|
||||
Nivå 1
|
||||
</Item>
|
||||
</AccordionMenu>
|
||||
);
|
||||
|
||||
ReactRouter.decorators = [
|
||||
(Story) => (
|
||||
<Router>
|
||||
<div
|
||||
style={{
|
||||
background: "white",
|
||||
maxWidth: 288,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
</Router>
|
||||
),
|
||||
];
|
||||
@@ -1,12 +0,0 @@
|
||||
.navds-story-menu {
|
||||
margin-top: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.navds-story-menu li:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.navds-story-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -1,3 +1,13 @@
|
||||
export {
|
||||
AccordionAnchorMenu,
|
||||
AccordionAnchorMenuCollapsable,
|
||||
AccordionAnchorMenuItem,
|
||||
} from "./accordion-anchor-menu";
|
||||
export {
|
||||
AccordionMenu,
|
||||
AccordionMenuCollapsable,
|
||||
AccordionMenuItem,
|
||||
} from "./accordion-menu";
|
||||
export { Accordion, AccordionProps } from "./accordion/index";
|
||||
export { Alert, AlertProps } from "./alert/index";
|
||||
export { Button, ButtonProps } from "./button/index";
|
||||
@@ -19,11 +29,6 @@ export {
|
||||
ProductPageLayoutProps,
|
||||
ProductPageLayoutSectionProps,
|
||||
} from "./layouts/index";
|
||||
export {
|
||||
AccordionMenu,
|
||||
AccordionMenuProps,
|
||||
AccordionMenuItemProps,
|
||||
} from "./accordion-menu";
|
||||
export { Link, LinkProps } from "./link/index";
|
||||
export { Modal, ModalProps } from "./modal/index";
|
||||
export { Popover, PopoverProps } from "./popover/index";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"private": false,
|
||||
"description": "NAV designsystem react components",
|
||||
"author": "NAV Designsystem team",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"private": false,
|
||||
"description": "Auto-generated design-tokens for NAV design-systems framework",
|
||||
"author": "NAV Designsystem team",
|
||||
|
||||
@@ -25,8 +25,12 @@ module.exports = {
|
||||
white: { value: white },
|
||||
darkgray: { value: darkgray },
|
||||
gray: {
|
||||
40: { value: "#b7b1a9" },
|
||||
60: { value: "#78706a" },
|
||||
90: { value: "#262626" },
|
||||
80: { value: "#4F4F4F" },
|
||||
60: { value: "#6A6A6A" },
|
||||
40: { value: "#A0A0A0" },
|
||||
20: { value: "#C9C9C9" },
|
||||
10: { value: "#F1F1F1" },
|
||||
},
|
||||
...Object.entries(baseColors).reduce(
|
||||
(colors, [name, color]) => ({
|
||||
|
||||
+5
-1
@@ -54,7 +54,10 @@
|
||||
]
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-standard",
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-prettier"
|
||||
],
|
||||
"rules": {
|
||||
"value-list-comma-newline-after": null,
|
||||
"declaration-colon-newline-after": null,
|
||||
@@ -135,6 +138,7 @@
|
||||
"less-loader": "^7.0.2",
|
||||
"mini-css-extract-plugin": "^1.3.0",
|
||||
"prettier": "2.2.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-docgen-typescript": "^1.21.0",
|
||||
|
||||
@@ -22802,6 +22802,11 @@ stylehacks@^4.0.0:
|
||||
postcss "^7.0.0"
|
||||
postcss-selector-parser "^3.0.0"
|
||||
|
||||
stylelint-config-prettier@^8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-config-prettier/-/stylelint-config-prettier-8.0.2.tgz#da9de33da4c56893cbe7e26df239a7374045e14e"
|
||||
integrity sha512-TN1l93iVTXpF9NJstlvP7nOu9zY2k+mN0NSFQ/VEGz15ZIP9ohdDZTtCWHs5LjctAhSAzaILULGbgiM0ItId3A==
|
||||
|
||||
stylelint-config-recommended@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz#e0e547434016c5539fe2650afd58049a2fd1d657"
|
||||
|
||||
Reference in new issue
Block a user