Compare commits

..
Author SHA1 Message Date
KenandCopilot 6b89d3540e refactor: Restructured timeline
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-21 21:31:49 +02:00
KenandCopilot ea14000efd refactor: Restructured table
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-21 21:31:49 +02:00
KenandCopilot 88c245e6b9 refactor: Restructured pagination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-21 21:31:48 +02:00
Ken a45e9cc97b fix: Update chips exports and sync skill 2026-10-21 21:31:48 +02:00
Ken 594373bde4 refactor: Restructured modal, popover 2026-10-21 21:31:47 +02:00
Ken 955589ca0e refactor: Restructured expansion-card, link-card, list 2026-10-21 21:31:46 +02:00
111 changed files with 1637 additions and 1442 deletions

No files matched your search

@@ -95,7 +95,7 @@ Move stories to component root dir. If stories import from internal paths, updat
### 5. Update `index.ts`
All exports are **named** — no `default` exports anywhere. The component `index.ts` re-exports the compound component, its sub-components, and their types from the root file:
All exports are **named** — no `default` exports anywhere. For compound components, the root file is the single export source: it assembles the compound component, imports every publicly exported sub-component and its prop types, and exports all public values and types together. The component `index.ts` only re-exports these names from `./root/<Component>Root` — never directly from sub-component files or shared type files.
```ts
"use client";
@@ -111,13 +111,18 @@ export type {
} from "./root/<Component>Root";
```
Group values first, then types. If types live in `<Component>.types.ts`, re-export them from there.
Group values first, then types. If types live in `<Component>.types.ts`, import them into the root file and include them in its type export block. The `index.ts` still re-exports them from the root file.
#### Compound root file export pattern
In `<Component>Root.tsx`, assemble the compound component with `Object.assign` — **do not** use an `as <Component>Component` cast or a dedicated `<Component>Component` interface:
```tsx
import {
<Component>Trigger,
type <Component>TriggerProps,
} from "../trigger/<Component>Trigger";
const <Component>Root = forwardRef<HTMLDivElement, <Component>Props>((props, ref) => {
// ...
});
@@ -138,6 +143,8 @@ export { <Component>, <Component>Trigger };
export type { <Component>Props, <Component>TriggerProps };
```
Follow `accordion/root/AccordionRoot.tsx`: export the imported sub-component values and prop types alongside the compound component at the bottom of the root file. Do not add internal-only sub-components to the public export chain. Sub-components must not import from the root file or component `index.ts`; use shared type/context/helper files to avoid circular dependencies.
Why `Object.assign` instead of `as`: the cast types the const as an interface that has no runtime `valueDeclaration`, so `react-docgen-typescript` (used by `yarn docgen:meta`) fails to extract props and can't document the component when it is exported via a grouped `export { }` statement. `Object.assign` lets TypeScript infer the intersection type from the value, so metadata generation works with named, bottom-of-file exports.
Sub-component files follow the same rule — named value export plus named type export at the bottom, no inline `export interface` and no default:
@@ -173,6 +180,8 @@ Concretely: grep for `export` lines in both versions and diff them. All componen
**Package root.** Verify `@navikt/core/react/src/index.ts` still re-exports the component. No changes needed there unless the component's `index.ts` path changed (it shouldn't).
**Root-only re-exports.** For compound components, verify every value and type export in the component `index.ts` comes from `./root/<Component>Root`, and every forwarded name is exported by that root file.
**No case-duplicate tracked paths.** This is the most common restructure bug. On case-insensitive filesystems (macOS, Windows) git can end up tracking **two** entries that differ only in casing (e.g. `accordion.stories.tsx` **and** `Accordion.stories.tsx`) while the working tree shows only one file. The stale entry then breaks tests and checkout on other machines. `ls` / file search will NOT reveal it — you must ask git. Run:
```sh
@@ -281,6 +290,7 @@ dialog/
- **No breaking changes.** Public export names and prop shapes must be identical after restructuring.
- **Named exports only.** No `default` exports; group named exports at the bottom of each file (values first, then types).
- **Compound exports flow through the root.** Component `index.ts` re-exports public values and types only from `root/<Component>Root.tsx`, not directly from sub-component or shared type files.
- **Compound roots use `Object.assign`.** No `as <Component>Component` cast or `<Component>Component` interface — it breaks `yarn docgen:meta` prop extraction.
- Component rules (React 17 compatibility, `"use client"`, `forwardRef`/`className`/`...rest`/`as`, tokens) live in `.github/instructions/ds-react-component.instructions.md`. They still apply.
- Keep JSDoc on public props and components. The component-level JSDoc block sits directly above the `Object.assign` call; sub-component `@see` tags go on the keys inside the `Object.assign` object literal.
@@ -289,6 +299,6 @@ dialog/
- Canonical example: `@navikt/core/react/src/dialog/`
- Package public API: `@navikt/core/react/src/index.ts`
- Component `index.ts` pattern: `@navikt/core/react/src/dialog/index.ts`
- Component export pattern: `@navikt/core/react/src/accordion/root/AccordionRoot.tsx` and `@navikt/core/react/src/accordion/index.ts`
- Context pattern: `@navikt/core/react/src/dialog/root/DialogRoot.context.ts`
- Meta: `@navikt/core/react/src/accordion/Accordion.meta.ts`
@@ -1,5 +1,5 @@
import React, { useRef } from "react";
import { useModalContext } from "../../modal/Modal.context";
import { useModalContext } from "../../modal/root/ModalRoot.context";
import { useId } from "../../utils-external";
import {
Menu,
+6 -6
View File
@@ -1,7 +1,7 @@
"use client";
export { Chips } from "./root/ChipsRoot";
export { ChipsRemovable } from "./removable/ChipsRemovable";
export { ChipsToggle } from "./toggle/ChipsToggle";
export type { ChipsProps } from "./root/ChipsRoot";
export type { ChipsRemovableProps } from "./removable/ChipsRemovable";
export type { ChipsToggleProps } from "./toggle/ChipsToggle";
export { Chips, ChipsRemovable, ChipsToggle } from "./root/ChipsRoot";
export type {
ChipsProps,
ChipsRemovableProps,
ChipsToggleProps,
} from "./root/ChipsRoot";
@@ -1,7 +1,10 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import { cl } from "../../utils/helpers";
import { ChipsRemovable } from "../removable/ChipsRemovable";
import { ChipsToggle } from "../toggle/ChipsToggle";
import {
ChipsRemovable,
type ChipsRemovableProps,
} from "../removable/ChipsRemovable";
import { ChipsToggle, type ChipsToggleProps } from "../toggle/ChipsToggle";
interface ChipsProps extends HTMLAttributes<HTMLUListElement> {
children: React.ReactNode;
@@ -73,5 +76,5 @@ const Chips = Object.assign(ChipsRoot, {
Removable: ChipsRemovable,
});
export { Chips };
export type { ChipsProps };
export { Chips, ChipsRemovable, ChipsToggle };
export type { ChipsProps, ChipsRemovableProps, ChipsToggleProps };
@@ -3,7 +3,7 @@ import { isSameDay } from "date-fns";
import React, { useState } from "react";
import { Button } from "../button";
import { Dialog } from "../dialog";
import Modal from "../modal/Modal";
import { Modal } from "../modal";
import { Box } from "../primitives/box";
import { HGrid } from "../primitives/grid";
import { HStack, Spacer, VStack } from "../primitives/stack";
@@ -1,5 +1,5 @@
import React, { forwardRef } from "react";
import { useModalContext } from "../../modal/Modal.context";
import { useModalContext } from "../../modal/root/ModalRoot.context";
import { Portal, type PortalProps } from "../../portal";
import { DialogBackdropInternal } from "../backdrop/DialogBackdropInternal";
import { useDialogContext } from "../root/DialogRoot.context";
@@ -1,10 +1,10 @@
import React, { forwardRef, useContext } from "react";
import { useThemeInternal } from "../theme/Theme";
import { BodyLong } from "../typography";
import { cl } from "../utils/helpers";
import { ExpansionCardContext } from "./context";
import { useThemeInternal } from "../../theme/Theme";
import { BodyLong } from "../../typography";
import { cl } from "../../utils/helpers";
import { ExpansionCardContext } from "../root/ExpansionCardRoot.context";
export interface ExpansionCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
interface ExpansionCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
@@ -44,4 +44,5 @@ const ExpansionCardContent = forwardRef<
);
});
export default ExpansionCardContent;
export { ExpansionCardContent };
export type { ExpansionCardContentProps };
@@ -1,13 +0,0 @@
import { createContext } from "react";
export type ExpansionCardContextProps = {
open: boolean;
toggleOpen: () => void;
size: "medium" | "small";
};
export const ExpansionCardContext = createContext<ExpansionCardContextProps>({
open: false,
toggleOpen: () => {},
size: "medium",
});
@@ -1,13 +1,13 @@
import React, { forwardRef, useContext } from "react";
import { BodyLong } from "../typography";
import { cl } from "../utils/helpers";
import { ExpansionCardContext } from "./context";
import { BodyLong } from "../../typography";
import { cl } from "../../utils/helpers";
import { ExpansionCardContext } from "../root/ExpansionCardRoot.context";
export interface ExpansionCardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
interface ExpansionCardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
children: React.ReactNode;
}
export const ExpansionCardDescription = forwardRef<
const ExpansionCardDescription = forwardRef<
HTMLParagraphElement,
ExpansionCardDescriptionProps
>(({ className, ...rest }, ref) => {
@@ -31,4 +31,5 @@ export const ExpansionCardDescription = forwardRef<
);
});
export default ExpansionCardDescription;
export { ExpansionCardDescription };
export type { ExpansionCardDescriptionProps };
@@ -1,10 +1,10 @@
import React, { forwardRef, useContext } from "react";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { cl } from "../utils/helpers";
import { useI18n } from "../utils/i18n/i18n.hooks";
import { ExpansionCardContext } from "./context";
import { cl } from "../../utils/helpers";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import { ExpansionCardContext } from "../root/ExpansionCardRoot.context";
export interface ExpansionCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
interface ExpansionCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
@@ -46,4 +46,5 @@ const ExpansionCardHeader = forwardRef<
);
});
export default ExpansionCardHeader;
export { ExpansionCardHeader };
export type { ExpansionCardHeaderProps };
@@ -1,9 +1,9 @@
"use client";
export {
type ExpansionCardProps,
default as ExpansionCard,
} from "./ExpansionCard";
export { default as ExpansionCardContent } from "./ExpansionCardContent";
export { default as ExpansionCardHeader } from "./ExpansionCardHeader";
export { default as ExpansionCardTitle } from "./ExpansionCardTitle";
export { default as ExpansionCardDescription } from "./ExpansionCardDescription";
ExpansionCard,
ExpansionCardContent,
ExpansionCardHeader,
ExpansionCardTitle,
ExpansionCardDescription,
} from "./root/ExpansionCardRoot";
export type { ExpansionCardProps } from "./root/ExpansionCardRoot";
@@ -0,0 +1,16 @@
import { createContext } from "react";
type ExpansionCardContextProps = {
open: boolean;
toggleOpen: () => void;
size: "medium" | "small";
};
const ExpansionCardContext = createContext<ExpansionCardContextProps>({
open: false,
toggleOpen: () => {},
size: "medium",
});
export { ExpansionCardContext };
export type { ExpansionCardContextProps };
@@ -1,51 +1,24 @@
import React, { forwardRef } from "react";
import type { AkselColor } from "../types";
import type { OverridableComponent } from "../utils-external";
import { cl } from "../utils/helpers";
import { useControllableState } from "../utils/hooks";
import ExpansionCardContent, {
import type { AkselColor } from "../../types";
import { cl } from "../../utils/helpers";
import { useControllableState } from "../../utils/hooks";
import {
ExpansionCardContent,
type ExpansionCardContentProps,
} from "./ExpansionCardContent";
} from "../content/ExpansionCardContent";
import {
ExpansionCardDescription,
type ExpansionCardDescriptionProps,
} from "./ExpansionCardDescription";
import ExpansionCardHeader, {
} from "../description/ExpansionCardDescription";
import {
ExpansionCardHeader,
type ExpansionCardHeaderProps,
} from "./ExpansionCardHeader";
} from "../header/ExpansionCardHeader";
import {
ExpansionCardTitle,
type ExpansionCardTitleProps,
} from "./ExpansionCardTitle";
import { ExpansionCardContext } from "./context";
interface ExpansionCardComponent extends React.ForwardRefExoticComponent<
ExpansionCardProps & React.RefAttributes<HTMLDivElement>
> {
/**
* @see 🏷️ {@link ExpansionCardHeaderProps}
*/
Header: React.ForwardRefExoticComponent<
ExpansionCardHeaderProps & React.RefAttributes<HTMLDivElement>
>;
/**
* @see 🏷️ {@link ExpansionCardTitleProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*/
Title: OverridableComponent<ExpansionCardTitleProps, HTMLHeadingElement>;
/**
* @see 🏷️ {@link ExpansionCardDescriptionProps}
*/
Description: React.ForwardRefExoticComponent<
ExpansionCardDescriptionProps & React.RefAttributes<HTMLParagraphElement>
>;
/**
* @see 🏷️ {@link ExpansionCardContentProps}
*/
Content: React.ForwardRefExoticComponent<
ExpansionCardContentProps & React.RefAttributes<HTMLDivElement>
>;
}
} from "../title/ExpansionCardTitle";
import { ExpansionCardContext } from "./ExpansionCardRoot.context";
interface ExpansionCardCommonProps extends Omit<
React.HTMLAttributes<HTMLDivElement>,
@@ -93,28 +66,10 @@ type ExpansionCardConditionalProps =
"aria-labelledby": string;
};
export type ExpansionCardProps = ExpansionCardCommonProps &
type ExpansionCardProps = ExpansionCardCommonProps &
ExpansionCardConditionalProps;
/**
* A component that displays an expandable card.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/expansioncard)
* @see 🏷️ {@link ExpansionCardProps}
*
* @example
* ```jsx
* <ExpansionCard aria-label="Utbetaling av sykepenger">
* <ExpansionCard.Header>
* <ExpansionCard.Title>Utbetaling av sykepenger</ExpansionCard.Title>
* </ExpansionCard.Header>
* <ExpansionCard.Content>
* <Innhold />
* </ExpansionCard.Content>
* </ExpansionCard>
* ```
*/
export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
const ExpansionCardRoot = forwardRef<HTMLDivElement, ExpansionCardProps>(
(
{
className,
@@ -154,11 +109,57 @@ export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
</ExpansionCardContext.Provider>
);
},
) as ExpansionCardComponent;
);
ExpansionCard.Header = ExpansionCardHeader;
ExpansionCard.Content = ExpansionCardContent;
ExpansionCard.Title = ExpansionCardTitle;
ExpansionCard.Description = ExpansionCardDescription;
/**
* A component that displays an expandable card.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/expansioncard)
* @see 🏷️ {@link ExpansionCardProps}
*
* @example
* ```jsx
* <ExpansionCard aria-label="Utbetaling av sykepenger">
* <ExpansionCard.Header>
* <ExpansionCard.Title>Utbetaling av sykepenger</ExpansionCard.Title>
* </ExpansionCard.Header>
* <ExpansionCard.Content>
* <Innhold />
* </ExpansionCard.Content>
* </ExpansionCard>
* ```
*/
const ExpansionCard = Object.assign(ExpansionCardRoot, {
/**
* @see 🏷️ {@link ExpansionCardHeaderProps}
*/
Header: ExpansionCardHeader,
/**
* @see 🏷️ {@link ExpansionCardTitleProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*/
Title: ExpansionCardTitle,
/**
* @see 🏷️ {@link ExpansionCardDescriptionProps}
*/
Description: ExpansionCardDescription,
/**
* @see 🏷️ {@link ExpansionCardContentProps}
*/
Content: ExpansionCardContent,
});
export default ExpansionCard;
export {
ExpansionCard,
ExpansionCardHeader,
ExpansionCardTitle,
ExpansionCardDescription,
ExpansionCardContent,
};
export type {
ExpansionCardProps,
ExpansionCardHeaderProps,
ExpansionCardTitleProps,
ExpansionCardDescriptionProps,
ExpansionCardContentProps,
};
@@ -1,8 +1,8 @@
import React, { forwardRef } from "react";
import type { OverridableComponent } from "../utils-external";
import { cl } from "../utils/helpers";
import type { OverridableComponent } from "../../utils-external";
import { cl } from "../../utils/helpers";
export interface ExpansionCardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
interface ExpansionCardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
children: React.ReactNode;
/**
* Changes text-sizing
@@ -11,7 +11,7 @@ export interface ExpansionCardTitleProps extends React.HTMLAttributes<HTMLHeadin
size?: "large" | "medium" | "small";
}
export const ExpansionCardTitle: OverridableComponent<
const ExpansionCardTitle: OverridableComponent<
ExpansionCardTitleProps,
HTMLHeadingElement
> = forwardRef(
@@ -32,4 +32,5 @@ export const ExpansionCardTitle: OverridableComponent<
},
);
export default ExpansionCardTitle;
export { ExpansionCardTitle };
export type { ExpansionCardTitleProps };
@@ -13,7 +13,7 @@ import {
LinkCardIcon,
LinkCardImage,
LinkCardTitle,
} from "./LinkCard";
} from "./index";
type Story = StoryObj<typeof LinkCard>;
@@ -1,334 +0,0 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import type { AkselColor } from "../types";
import { BodyLong, Heading } from "../typography";
import {
LinkAnchor,
LinkAnchorArrow,
LinkAnchorOverlay,
type LinkAnchorProps,
} from "../utils/components/link-anchor";
import { cl, createStrictContext } from "../utils/helpers";
/* ------------------------------ LinkCard Root ----------------------------- */
interface LinkCardProps extends HTMLAttributes<HTMLDivElement> {
/**
* @default true
*/
arrow?: boolean;
/**
* Adjusts arrow position.
* @default "baseline"
*/
arrowPosition?: "baseline" | "center";
/**
* Changes padding and typo sizes.
* @default "medium"
*/
size?: "small" | "medium";
/**
* Overrides color.
*
* We recommend avoiding status-colors (`info`, `success`, `warning`, `danger`) in LinkCards.
* @default "neutral"
* @see 🏷️ {@link AkselColor}
* @see [📝 Documentation](https://aksel.nav.no/grunnleggende/styling/farger-tokens)
*/
"data-color"?: AkselColor;
/**
* Changes the HTML element used for the root element.
*
* **When using `section`, provide either `aria-label` or `aria-labelledby` for better accessibility.**
* `axe-core` might warn about unique landmarks if you have multiple Accordions on page with the same label.
* In those cases consider updating to unique `aria-label` or `aria-labelledby` props.
* @see [📝 Landmarks unique](https://dequeuniversity.com/rules/axe/4.6/landmark-unique)
*
*
* **When using `article`, make sure `<LinkCard.Title />` is a heading and not a `span`.**
* @default "div"
*/
as?: "div" | "section" | "article";
}
type LinkCardContextProps = {
size: LinkCardProps["size"];
};
const { Provider: LinkCardContextProvider, useContext: useLinkCardContext } =
createStrictContext<LinkCardContextProps>({
name: "LinkCardContextProvider",
});
interface LinkCardComponent extends React.ForwardRefExoticComponent<
LinkCardProps & React.RefAttributes<HTMLDivElement>
> {
/**
* @see 🏷️ {@link LinkCardTitleProps}
*/
Title: typeof LinkCardTitle;
/**
* @see 🏷️ {@link LinkCardAnchorProps}
*/
Anchor: typeof LinkCardAnchor;
/**
* @see 🏷️ {@link LinkCardDescriptionProps}
*/
Description: typeof LinkCardDescription;
/**
* @see 🏷️ {@link LinkCardFooterProps}
*/
Footer: typeof LinkCardFooter;
/**
* @see 🏷️ {@link LinkCardIconProps}
*/
Icon: typeof LinkCardIcon;
/**
* @see 🏷️ {@link LinkCardImageProps}
*/
Image: typeof LinkCardImage;
}
/**
* Accessible clickable card as a link.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/linkcard)
* @see 🏷️ {@link LinkCardProps}
*
*
* @example
* ```tsx
* <LinkCard>
* <LinkCard.Icon>
* <IconOrPictogram />
* </LinkCard.Icon>
* <LinkCard.Title>
* <LinkCard.Anchor href="/href">
* LinkCard title
* </LinkCard.Anchor>
* </LinkCard.Title>
* <LinkCard.Description>
* This is a description of the link card.
* </LinkCard.Description>
* <LinkCard.Footer>Footer content</LinkCard.Footer>
* </LinkCard>
* ```
*/
export const LinkCard = forwardRef<HTMLDivElement, LinkCardProps>(
(
{
children,
className,
arrow = true,
arrowPosition = "baseline",
size = "medium",
as: Component = "div",
...restProps
}: LinkCardProps,
forwardedRef,
) => {
return (
<LinkCardContextProvider size={size}>
<LinkAnchorOverlay asChild>
<BodyLong
as={Component}
size={size}
ref={forwardedRef}
data-color="neutral"
className={cl(
"aksel-link-card",
className,
`aksel-link-card--${size}`,
)}
data-align-arrow={arrowPosition}
{...restProps}
>
{children}
{arrow && (
<LinkAnchorArrow
fontSize={size === "medium" ? "1.75rem" : "1.5rem"}
className="aksel-link-card__arrow"
/>
)}
</BodyLong>
</LinkAnchorOverlay>
</LinkCardContextProvider>
);
},
) as LinkCardComponent;
/* ---------------------------- LinkCard Title ---------------------------- */
type LinkCardTitleProps = HTMLAttributes<HTMLHeadingElement> & {
children: React.ReactNode;
/**
* Heading tag. Use "span" if you want a non header defining card
* (eg. you have a lot of them all at once, such as in a grid)
* @default "span"
*/
as?: "span" | "h2" | "h3" | "h4" | "h5" | "h6";
};
/**
* @see 🏷️ {@link LinkCardTitleProps}
*/
export const LinkCardTitle = forwardRef<HTMLHeadingElement, LinkCardTitleProps>(
(
{ children, as = "span", className, ...restProps }: LinkCardTitleProps,
forwardedRef,
) => {
const context = useLinkCardContext();
return (
<Heading
ref={forwardedRef}
as={as}
size={context.size === "medium" ? "small" : "xsmall"}
className={cl("aksel-link-card__title", className)}
{...restProps}
>
{children}
</Heading>
);
},
);
/* ---------------------------- LinkCard Anchor ---------------------------- */
type LinkCardAnchorProps = LinkAnchorProps;
/**
* @see 🏷️ {@link LinkCardAnchorProps}
*/
export const LinkCardAnchor = LinkAnchor;
/* ---------------------------- LinkCard Description ---------------------------- */
interface LinkCardDescriptionProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @see 🏷️ {@link LinkCardDescriptionProps}
*/
export const LinkCardDescription = forwardRef<
HTMLDivElement,
LinkCardDescriptionProps
>(
(
{ children, className, ...restProps }: LinkCardDescriptionProps,
forwardedRef,
) => {
return (
<div
ref={forwardedRef}
className={cl("aksel-link-card__description", className)}
{...restProps}
>
{children}
</div>
);
},
);
/* ---------------------------- LinkCard Footer ---------------------------- */
interface LinkCardFooterProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @see 🏷️ {@link LinkCardFooterProps}
*/
export const LinkCardFooter = forwardRef<HTMLDivElement, LinkCardFooterProps>(
(
{ children, className, ...restProps }: LinkCardFooterProps,
forwardedRef,
) => {
return (
<div
ref={forwardedRef}
className={cl("aksel-link-card__footer", className)}
{...restProps}
>
{children}
</div>
);
},
);
/* ---------------------------- LinkCard Icon ---------------------------- */
interface LinkCardIconProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @see 🏷️ {@link LinkCardIconProps}
*/
export const LinkCardIcon = forwardRef<HTMLDivElement, LinkCardIconProps>(
({ children, className, ...restProps }: LinkCardIconProps, forwardedRef) => {
return (
<div
ref={forwardedRef}
aria-hidden
className={cl("aksel-link-card__icon", className)}
{...restProps}
>
{children}
</div>
);
},
);
/* ---------------------------- LinkCard Image ---------------------------- */
type ImageAspectRatio = "1/1" | "16/9" | "16/10" | "4/3" | (string & {});
interface LinkCardImageProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* The aspect-ratio CSS property allows you to define the desired width-to-height ratio of an element's box.
* This means that even if the parent container or viewport size changes, the browser will adjust the element's dimensions to maintain the specified width-to-height ratio.
*/
aspectRatio?: ImageAspectRatio;
}
/**
* @see 🏷️ {@link LinkCardImageProps}
*/
export const LinkCardImage = forwardRef<HTMLDivElement, LinkCardImageProps>(
(
{
children,
className,
aspectRatio,
style,
...restProps
}: LinkCardImageProps,
forwardedRef,
) => {
return (
<div
ref={forwardedRef}
className={cl("aksel-link-card__image-container", className)}
style={{
...style,
aspectRatio,
}}
{...restProps}
>
{children}
</div>
);
},
);
LinkCard.Title = LinkCardTitle;
LinkCard.Anchor = LinkCardAnchor;
LinkCard.Description = LinkCardDescription;
LinkCard.Footer = LinkCardFooter;
LinkCard.Icon = LinkCardIcon;
LinkCard.Image = LinkCardImage;
export type {
LinkCardAnchorProps,
LinkCardDescriptionProps,
LinkCardFooterProps,
LinkCardIconProps,
LinkCardImageProps,
LinkCardProps,
LinkCardTitleProps,
};
@@ -0,0 +1,14 @@
import {
LinkAnchor,
type LinkAnchorProps,
} from "../../utils/components/link-anchor";
type LinkCardAnchorProps = LinkAnchorProps;
/**
* @see 🏷️ {@link LinkCardAnchorProps}
*/
const LinkCardAnchor = LinkAnchor;
export { LinkCardAnchor };
export type { LinkCardAnchorProps };
@@ -0,0 +1,32 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import { cl } from "../../utils/helpers";
interface LinkCardDescriptionProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @see 🏷️ {@link LinkCardDescriptionProps}
*/
const LinkCardDescription = forwardRef<
HTMLDivElement,
LinkCardDescriptionProps
>(
(
{ children, className, ...restProps }: LinkCardDescriptionProps,
forwardedRef,
) => {
return (
<div
ref={forwardedRef}
className={cl("aksel-link-card__description", className)}
{...restProps}
>
{children}
</div>
);
},
);
export { LinkCardDescription };
export type { LinkCardDescriptionProps };
@@ -0,0 +1,29 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import { cl } from "../../utils/helpers";
interface LinkCardFooterProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @see 🏷️ {@link LinkCardFooterProps}
*/
const LinkCardFooter = forwardRef<HTMLDivElement, LinkCardFooterProps>(
(
{ children, className, ...restProps }: LinkCardFooterProps,
forwardedRef,
) => {
return (
<div
ref={forwardedRef}
className={cl("aksel-link-card__footer", className)}
{...restProps}
>
{children}
</div>
);
},
);
export { LinkCardFooter };
export type { LinkCardFooterProps };
@@ -0,0 +1,27 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import { cl } from "../../utils/helpers";
interface LinkCardIconProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @see 🏷️ {@link LinkCardIconProps}
*/
const LinkCardIcon = forwardRef<HTMLDivElement, LinkCardIconProps>(
({ children, className, ...restProps }: LinkCardIconProps, forwardedRef) => {
return (
<div
ref={forwardedRef}
aria-hidden
className={cl("aksel-link-card__icon", className)}
{...restProps}
>
{children}
</div>
);
},
);
export { LinkCardIcon };
export type { LinkCardIconProps };
@@ -0,0 +1,46 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import { cl } from "../../utils/helpers";
type ImageAspectRatio = "1/1" | "16/9" | "16/10" | "4/3" | (string & {});
interface LinkCardImageProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* The aspect-ratio CSS property allows you to define the desired width-to-height ratio of an element's box.
* This means that even if the parent container or viewport size changes, the browser will adjust the element's dimensions to maintain the specified width-to-height ratio.
*/
aspectRatio?: ImageAspectRatio;
}
/**
* @see 🏷️ {@link LinkCardImageProps}
*/
const LinkCardImage = forwardRef<HTMLDivElement, LinkCardImageProps>(
(
{
children,
className,
aspectRatio,
style,
...restProps
}: LinkCardImageProps,
forwardedRef,
) => {
return (
<div
ref={forwardedRef}
className={cl("aksel-link-card__image-container", className)}
style={{
...style,
aspectRatio,
}}
{...restProps}
>
{children}
</div>
);
},
);
export { LinkCardImage };
export type { LinkCardImageProps };
+2 -2
View File
@@ -7,7 +7,7 @@ export {
LinkCardAnchor,
LinkCardIcon,
LinkCardImage,
} from "./LinkCard";
} from "./root/LinkCardRoot";
export type {
LinkCardProps,
@@ -17,4 +17,4 @@ export type {
LinkCardAnchorProps,
LinkCardIconProps,
LinkCardImageProps,
} from "./LinkCard";
} from "./root/LinkCardRoot";
@@ -0,0 +1,14 @@
import { createStrictContext } from "../../utils/helpers";
import type { LinkCardProps } from "./LinkCardRoot";
type LinkCardContextProps = {
size: LinkCardProps["size"];
};
const { Provider: LinkCardContextProvider, useContext: useLinkCardContext } =
createStrictContext<LinkCardContextProps>({
name: "LinkCardContextProvider",
});
export { LinkCardContextProvider, useLinkCardContext };
export type { LinkCardContextProps };
@@ -0,0 +1,177 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import type { AkselColor } from "../../types";
import { BodyLong } from "../../typography";
import {
LinkAnchorArrow,
LinkAnchorOverlay,
} from "../../utils/components/link-anchor";
import { cl } from "../../utils/helpers";
import {
LinkCardAnchor,
type LinkCardAnchorProps,
} from "../anchor/LinkCardAnchor";
import {
LinkCardDescription,
type LinkCardDescriptionProps,
} from "../description/LinkCardDescription";
import {
LinkCardFooter,
type LinkCardFooterProps,
} from "../footer/LinkCardFooter";
import { LinkCardIcon, type LinkCardIconProps } from "../icon/LinkCardIcon";
import { LinkCardImage, type LinkCardImageProps } from "../image/LinkCardImage";
import { LinkCardTitle, type LinkCardTitleProps } from "../title/LinkCardTitle";
import { LinkCardContextProvider } from "./LinkCardRoot.context";
interface LinkCardProps extends HTMLAttributes<HTMLDivElement> {
/**
* @default true
*/
arrow?: boolean;
/**
* Adjusts arrow position.
* @default "baseline"
*/
arrowPosition?: "baseline" | "center";
/**
* Changes padding and typo sizes.
* @default "medium"
*/
size?: "small" | "medium";
/**
* Overrides color.
*
* We recommend avoiding status-colors (`info`, `success`, `warning`, `danger`) in LinkCards.
* @default "neutral"
* @see 🏷️ {@link AkselColor}
* @see [📝 Documentation](https://aksel.nav.no/grunnleggende/styling/farger-tokens)
*/
"data-color"?: AkselColor;
/**
* Changes the HTML element used for the root element.
*
* **When using `section`, provide either `aria-label` or `aria-labelledby` for better accessibility.**
* `axe-core` might warn about unique landmarks if you have multiple Accordions on page with the same label.
* In those cases consider updating to unique `aria-label` or `aria-labelledby` props.
* @see [📝 Landmarks unique](https://dequeuniversity.com/rules/axe/4.6/landmark-unique)
*
*
* **When using `article`, make sure `<LinkCard.Title />` is a heading and not a `span`.**
* @default "div"
*/
as?: "div" | "section" | "article";
}
const LinkCardRoot = forwardRef<HTMLDivElement, LinkCardProps>(
(
{
children,
className,
arrow = true,
arrowPosition = "baseline",
size = "medium",
as: Component = "div",
...restProps
}: LinkCardProps,
forwardedRef,
) => {
return (
<LinkCardContextProvider size={size}>
<LinkAnchorOverlay asChild>
<BodyLong
as={Component}
size={size}
ref={forwardedRef}
data-color="neutral"
className={cl(
"aksel-link-card",
className,
`aksel-link-card--${size}`,
)}
data-align-arrow={arrowPosition}
{...restProps}
>
{children}
{arrow && (
<LinkAnchorArrow
fontSize={size === "medium" ? "1.75rem" : "1.5rem"}
className="aksel-link-card__arrow"
/>
)}
</BodyLong>
</LinkAnchorOverlay>
</LinkCardContextProvider>
);
},
);
/**
* Accessible clickable card as a link.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/linkcard)
* @see 🏷️ {@link LinkCardProps}
*
*
* @example
* ```tsx
* <LinkCard>
* <LinkCard.Icon>
* <IconOrPictogram />
* </LinkCard.Icon>
* <LinkCard.Title>
* <LinkCard.Anchor href="/href">
* LinkCard title
* </LinkCard.Anchor>
* </LinkCard.Title>
* <LinkCard.Description>
* This is a description of the link card.
* </LinkCard.Description>
* <LinkCard.Footer>Footer content</LinkCard.Footer>
* </LinkCard>
* ```
*/
const LinkCard = Object.assign(LinkCardRoot, {
/**
* @see 🏷️ {@link LinkCardTitleProps}
*/
Title: LinkCardTitle,
/**
* @see 🏷️ {@link LinkCardAnchorProps}
*/
Anchor: LinkCardAnchor,
/**
* @see 🏷️ {@link LinkCardDescriptionProps}
*/
Description: LinkCardDescription,
/**
* @see 🏷️ {@link LinkCardFooterProps}
*/
Footer: LinkCardFooter,
/**
* @see 🏷️ {@link LinkCardIconProps}
*/
Icon: LinkCardIcon,
/**
* @see 🏷️ {@link LinkCardImageProps}
*/
Image: LinkCardImage,
});
export {
LinkCard,
LinkCardTitle,
LinkCardDescription,
LinkCardFooter,
LinkCardAnchor,
LinkCardIcon,
LinkCardImage,
};
export type {
LinkCardProps,
LinkCardTitleProps,
LinkCardDescriptionProps,
LinkCardFooterProps,
LinkCardAnchorProps,
LinkCardIconProps,
LinkCardImageProps,
};
@@ -0,0 +1,41 @@
import React, { type HTMLAttributes, forwardRef } from "react";
import { Heading } from "../../typography";
import { cl } from "../../utils/helpers";
import { useLinkCardContext } from "../root/LinkCardRoot.context";
type LinkCardTitleProps = HTMLAttributes<HTMLHeadingElement> & {
children: React.ReactNode;
/**
* Heading tag. Use "span" if you want a non header defining card
* (eg. you have a lot of them all at once, such as in a grid)
* @default "span"
*/
as?: "span" | "h2" | "h3" | "h4" | "h5" | "h6";
};
/**
* @see 🏷️ {@link LinkCardTitleProps}
*/
const LinkCardTitle = forwardRef<HTMLHeadingElement, LinkCardTitleProps>(
(
{ children, as = "span", className, ...restProps }: LinkCardTitleProps,
forwardedRef,
) => {
const context = useLinkCardContext();
return (
<Heading
ref={forwardedRef}
as={as}
size={context.size === "medium" ? "small" : "xsmall"}
className={cl("aksel-link-card__title", className)}
{...restProps}
>
{children}
</Heading>
);
},
);
export { LinkCardTitle };
export type { LinkCardTitleProps };
+1 -1
View File
@@ -9,7 +9,7 @@ import {
import { Box } from "../primitives/box";
import { VStack } from "../primitives/stack";
import { BodyLong } from "../typography";
import List from "./List";
import { List } from "./index";
export default {
title: "ds-react/List",
+4 -2
View File
@@ -1,4 +1,4 @@
export interface ListProps extends React.HTMLAttributes<HTMLDivElement> {
interface ListProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* HTML list element to render.
@@ -16,7 +16,7 @@ export interface ListProps extends React.HTMLAttributes<HTMLDivElement> {
size?: "small" | "medium" | "large";
}
export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
/**
* List item content.
*/
@@ -30,3 +30,5 @@ export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
*/
icon?: React.ReactNode;
}
export type { ListProps, ListItemProps };
+2 -2
View File
@@ -1,3 +1,3 @@
"use client";
export { default as List, ListItem } from "./List";
export type { ListProps, ListItemProps } from "./List";
export { List, ListItem } from "./root/ListRoot";
export type { ListProps, ListItemProps } from "./root/ListRoot";
@@ -1,14 +1,14 @@
import React, { forwardRef, useContext } from "react";
import { BodyLong } from "../typography";
import { cl } from "../utils/helpers";
import { consoleWarning } from "../utils/helpers/consoleWarning";
import { ListContext } from "./List.context";
import type { ListItemProps } from "./List.types";
import { BodyLong } from "../../typography";
import { cl } from "../../utils/helpers";
import { consoleWarning } from "../../utils/helpers/consoleWarning";
import type { ListItemProps } from "../List.types";
import { ListContext } from "../root/ListRoot.context";
/**
* @see 🏷️ {@link ListItemProps}
*/
export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
({ className, children, title, icon, ...rest }, ref) => {
const { listType, size } = useContext(ListContext);
@@ -57,4 +57,6 @@ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
);
ListItem.displayName = "List.Item";
export default ListItem;
export { ListItem };
export type { ListItemProps };
@@ -1,12 +1,14 @@
import { createContext } from "react";
import type { ListProps } from "./List.types";
import type { ListProps } from "../List.types";
interface ListContextProps {
listType: Exclude<ListProps["as"], undefined>;
size: Exclude<ListProps["size"], undefined>;
}
export const ListContext = createContext<ListContextProps>({
const ListContext = createContext<ListContextProps>({
listType: "ul",
size: "medium",
});
export { ListContext };
@@ -1,35 +1,11 @@
import React, { forwardRef, useContext } from "react";
import { BodyLong } from "../typography";
import { cl } from "../utils/helpers";
import { ListItem } from "./List.Item";
import { ListContext } from "./List.context";
import type { ListItemProps, ListProps } from "./List.types";
import { BodyLong } from "../../typography";
import { cl } from "../../utils/helpers";
import type { ListItemProps, ListProps } from "../List.types";
import { ListItem } from "../item/ListItem";
import { ListContext } from "./ListRoot.context";
interface ListComponent extends React.ForwardRefExoticComponent<
ListProps & React.RefAttributes<HTMLDivElement>
> {
/**
* @see 🏷️ {@link ListItemProps}
*/
Item: typeof ListItem;
}
/**
* A list component
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/list)
* @see 🏷️ {@link ListProps | Props}
*
* @example
* ```jsx
* <List>
* <List.Item>Coffee</List.Item>
* <List.Item>Tea</List.Item>
* <List.Item>Milk</List.Item>
* </List>
* ```
*/
export const List = forwardRef<HTMLDivElement, ListProps>(
const ListRoot = forwardRef<HTMLDivElement, ListProps>(
(
{
children,
@@ -71,10 +47,29 @@ export const List = forwardRef<HTMLDivElement, ListProps>(
</ListContext.Provider>
);
},
) as ListComponent;
);
List.Item = ListItem;
/**
* A list component
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/list)
* @see 🏷️ {@link ListProps | Props}
*
* @example
* ```jsx
* <List>
* <List.Item>Coffee</List.Item>
* <List.Item>Tea</List.Item>
* <List.Item>Milk</List.Item>
* </List>
* ```
*/
const List = Object.assign(ListRoot, {
/**
* @see 🏷️ {@link ListItemProps}
*/
Item: ListItem,
});
export default List;
export { ListItem };
export { List, ListItem };
export type { ListProps, ListItemProps };
@@ -6,7 +6,7 @@ import { Checkbox, CheckboxGroup } from "../form/checkbox";
import { VStack } from "../primitives/stack";
import { Tooltip } from "../tooltip";
import { BodyLong, Heading } from "../typography";
import Modal from "./Modal";
import { Modal } from "./index";
const meta: Meta<typeof Modal> = {
title: "ds-react/Modal",
@@ -1,5 +1,5 @@
import { expectTypeOf } from "vitest";
import type { ModalProps } from "./types";
import type { ModalProps } from "./Modal.types";
test("ModalProps works as intended", () => {
expectTypeOf({
@@ -93,7 +93,7 @@ interface ModalPropsBase extends React.DialogHTMLAttributes<HTMLDialogElement> {
}
// Require onClose if you use open & Require either header, aria-labelledby or aria-label
export type ModalProps = ModalPropsBase &
type ModalProps = ModalPropsBase &
(
| { onClose: ModalPropsBase["onClose"]; open: boolean | undefined }
| { onClose?: ModalPropsBase["onClose"]; open?: never }
@@ -117,3 +117,5 @@ export type ModalProps = ModalPropsBase &
| { "aria-labelledby": string; "aria-label"?: never }
| { "aria-labelledby"?: never; "aria-label": string }
);
export type { ModalProps };
@@ -1,57 +0,0 @@
import React, { useEffect } from "react";
import type { ModalProps } from "./types";
export interface MouseCoordinates {
clientX: number;
clientY: number;
}
export const coordsAreInside = (
{ clientX, clientY }: MouseCoordinates,
{ left, top, right, bottom }: DOMRect,
) => {
if (clientX < left || clientY < top) return false;
if (clientX > right || clientY > bottom) return false;
return true;
};
export function getCloseHandler(
modalRef: React.RefObject<HTMLDialogElement | null>,
header: ModalProps["header"],
onBeforeClose: ModalProps["onBeforeClose"],
) {
if (header && header.closeButton === false) return undefined;
if (onBeforeClose) {
return () => onBeforeClose() !== false && modalRef.current?.close();
}
return () => modalRef.current?.close();
}
function useIsModalOpen(modalRef: HTMLDialogElement | null) {
const [isOpen, setIsOpen] = React.useState<boolean>(false);
useEffect(() => {
if (!modalRef) {
return;
}
setIsOpen(modalRef.open); // eslint-disable-line react-hooks/set-state-in-effect
const observer = new MutationObserver(() => {
setIsOpen(modalRef.open);
});
observer.observe(modalRef, {
attributes: true,
attributeFilter: ["open"],
});
return () => {
observer.disconnect();
};
}, [modalRef]);
return isOpen;
}
export { useIsModalOpen };
@@ -1,7 +1,7 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
import { cl } from "../../utils/helpers";
export interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> {
interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
@@ -13,4 +13,5 @@ const ModalBody = forwardRef<HTMLDivElement, ModalBodyProps>(
},
);
export default ModalBody;
export { ModalBody };
export type { ModalBodyProps };
@@ -1,7 +1,7 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
import { cl } from "../../utils/helpers";
export interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
@@ -17,4 +17,5 @@ const ModalFooter = forwardRef<HTMLDivElement, ModalFooterProps>(
},
);
export default ModalFooter;
export { ModalFooter };
export type { ModalFooterProps };
@@ -1,11 +1,11 @@
import React, { forwardRef } from "react";
import { XMarkIcon } from "@navikt/aksel-icons";
import { Button } from "../button";
import { cl } from "../utils/helpers";
import { useI18n } from "../utils/i18n/i18n.hooks";
import { useModalContext } from "./Modal.context";
import { Button } from "../../button";
import { cl } from "../../utils/helpers";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import { useModalContext } from "../root/ModalRoot.context";
export interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
/**
* Removes close-button (X) when false
@@ -43,4 +43,5 @@ const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
},
);
export default ModalHeader;
export { ModalHeader };
export type { ModalHeaderProps };
@@ -2,7 +2,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
export const needPolyfill =
const needPolyfill =
typeof window !== "undefined" &&
(window.HTMLDialogElement === undefined ||
navigator.userAgent.includes("jsdom"));
@@ -503,7 +503,10 @@ dialogPolyfillInfo.prototype = /** @type {HTMLDialogElement.prototype} */ {
},
};
var dialogPolyfill = {};
var dialogPolyfill = {} as {
registerDialog(dialog: HTMLDialogElement): void;
forceRegisterDialog(dialog: HTMLDialogElement): void;
};
dialogPolyfill.reposition = function (element) {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
@@ -947,7 +950,4 @@ if (needPolyfill) {
HTMLFormElement.prototype.submit = replacementFormSubmit;
}
export default dialogPolyfill as {
registerDialog(dialog: HTMLDialogElement): void;
forceRegisterDialog(dialog: HTMLDialogElement): void;
};
export { dialogPolyfill, needPolyfill };
@@ -0,0 +1,31 @@
import React from "react";
import type { ModalProps } from "../Modal.types";
interface MouseCoordinates {
clientX: number;
clientY: number;
}
const coordsAreInside = (
{ clientX, clientY }: MouseCoordinates,
{ left, top, right, bottom }: DOMRect,
) => {
if (clientX < left || clientY < top) return false;
if (clientX > right || clientY > bottom) return false;
return true;
};
function getCloseHandler(
modalRef: React.RefObject<HTMLDialogElement | null>,
header: ModalProps["header"],
onBeforeClose: ModalProps["onBeforeClose"],
) {
if (header && header.closeButton === false) return undefined;
if (onBeforeClose) {
return () => onBeforeClose() !== false && modalRef.current?.close();
}
return () => modalRef.current?.close();
}
export { coordsAreInside, getCloseHandler };
export type { MouseCoordinates };
+7 -5
View File
@@ -1,6 +1,8 @@
"use client";
export { default as Modal } from "./Modal";
export { default as ModalBody, type ModalBodyProps } from "./ModalBody";
export { default as ModalFooter, type ModalFooterProps } from "./ModalFooter";
export { default as ModalHeader, type ModalHeaderProps } from "./ModalHeader";
export type { ModalProps } from "./types";
export { Modal, ModalBody, ModalFooter, ModalHeader } from "./root/ModalRoot";
export type {
ModalProps,
ModalBodyProps,
ModalFooterProps,
ModalHeaderProps,
} from "./root/ModalRoot";
@@ -1,13 +1,15 @@
import React from "react";
import { createStrictContext } from "../utils/helpers";
import { createStrictContext } from "../../utils/helpers";
interface ModalContextProps {
closeHandler?: React.MouseEventHandler<HTMLButtonElement>;
modalRef: React.RefObject<HTMLDialogElement | null>;
}
export const { Provider: ModalContextProvider, useContext: useModalContext } =
const { Provider: ModalContextProvider, useContext: useModalContext } =
createStrictContext<ModalContextProps>({
name: "ModalContext",
errorMessage: "<Modal.Header> must be used within a <Modal>",
});
export { ModalContextProvider, useModalContext };
@@ -1,7 +1,8 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React, { useState } from "react";
import { describe, expect, test } from "vitest";
import { Button, Modal } from "..";
import { Button } from "../../button";
import { Modal } from "../index";
const Test = () => {
const [open, setOpen] = useState(true);
@@ -1,82 +1,28 @@
import { useFloatingPortalNode } from "@floating-ui/react";
import React, { forwardRef, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useProvider } from "../provider/Provider";
import { Detail, Heading } from "../typography";
import { useId } from "../utils-external";
import { useDateInputContext } from "../utils/components/date/input/DateInput";
import { cl, composeEventHandlers } from "../utils/helpers";
import { useMergeRefs, useScrollLock } from "../utils/hooks";
import { ModalContextProvider, useModalContext } from "./Modal.context";
import ModalBody from "./ModalBody";
import ModalFooter from "./ModalFooter";
import ModalHeader from "./ModalHeader";
import { useProvider } from "../../provider/Provider";
import { Detail, Heading } from "../../typography";
import { useId } from "../../utils-external";
import { useDateInputContext } from "../../utils/components/date/input/DateInput";
import { cl, composeEventHandlers } from "../../utils/helpers";
import { useMergeRefs, useScrollLock } from "../../utils/hooks";
import type { ModalProps } from "../Modal.types";
import { ModalBody, type ModalBodyProps } from "../body/ModalBody";
import { ModalFooter, type ModalFooterProps } from "../footer/ModalFooter";
import { ModalHeader, type ModalHeaderProps } from "../header/ModalHeader";
import { dialogPolyfill, needPolyfill } from "../helpers/dialog-polyfill";
import {
type MouseCoordinates,
coordsAreInside,
getCloseHandler,
useIsModalOpen,
} from "./ModalUtils";
import dialogPolyfill, { needPolyfill } from "./dialog-polyfill";
import type { ModalProps } from "./types";
} from "../helpers/modal-utils";
import { ModalContextProvider, useModalContext } from "./ModalRoot.context";
import { useIsModalOpen } from "./useIsModalOpen";
const polyfillClassName = "aksel-modal--polyfilled";
interface ModalComponent extends React.ForwardRefExoticComponent<
ModalProps & React.RefAttributes<HTMLDialogElement>
> {
Header: typeof ModalHeader;
Body: typeof ModalBody;
Footer: typeof ModalFooter;
}
/**
* A component that displays a modal dialog.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/modal)
* @see 🏷️ {@link ModalProps}
*
* @example
* State change with `useRef`
* ```jsx
* const ref = useRef<HTMLDialogElement>(null);
* <Button onClick={() => ref.current?.showModal()}>Open modal</Button>
* <Modal
* ref={ref}
* header={{
* label: "Optional label",
* icon: <FileIcon aria-hidden />,
* heading: "My heading",
* }}
* >
* <Modal.Body>
* <BodyLong>Hello world</BodyLong>
* </Modal.Body>
* <Modal.Footer>
* <Button>Save</Button>
* <Button type="button" variant="tertiary" onClick={() => ref.current?.close()}>Close</Button>
* </Modal.Footer>
* </Modal>
* ```
* @example
* State change with `useState`
* ```jsx
* const [open, setOpen] = useState(false);
* <Modal
* open={open}
* onClose={() => setOpen(false)}
* aria-labelledby="modal-heading"
* >
* <Modal.Header>
* <Heading level="1" size="large" id="modal-heading">My heading</Heading>
* </Modal.Header>
* <Modal.Body>
* <BodyLong>Hello world</BodyLong>
* </Modal.Body>
* </Modal>
* ```
*/
export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
const ModalRoot = forwardRef<HTMLDialogElement, ModalProps>(
(
{
header,
@@ -285,10 +231,68 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
}
return component;
},
) as ModalComponent;
);
Modal.Header = ModalHeader;
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
/**
* A component that displays a modal dialog.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/modal)
* @see 🏷️ {@link ModalProps}
*
* @example
* State change with `useRef`
* ```jsx
* const ref = useRef<HTMLDialogElement>(null);
* <Button onClick={() => ref.current?.showModal()}>Open modal</Button>
* <Modal
* ref={ref}
* header={{
* label: "Optional label",
* icon: <FileIcon aria-hidden />,
* heading: "My heading",
* }}
* >
* <Modal.Body>
* <BodyLong>Hello world</BodyLong>
* </Modal.Body>
* <Modal.Footer>
* <Button>Save</Button>
* <Button type="button" variant="tertiary" onClick={() => ref.current?.close()}>Close</Button>
* </Modal.Footer>
* </Modal>
* ```
* @example
* State change with `useState`
* ```jsx
* const [open, setOpen] = useState(false);
* <Modal
* open={open}
* onClose={() => setOpen(false)}
* aria-labelledby="modal-heading"
* >
* <Modal.Header>
* <Heading level="1" size="large" id="modal-heading">My heading</Heading>
* </Modal.Header>
* <Modal.Body>
* <BodyLong>Hello world</BodyLong>
* </Modal.Body>
* </Modal>
* ```
*/
const Modal = Object.assign(ModalRoot, {
/**
* @see 🏷️ {@link ModalHeaderProps}
*/
Header: ModalHeader,
/**
* @see 🏷️ {@link ModalBodyProps}
*/
Body: ModalBody,
/**
* @see 🏷️ {@link ModalFooterProps}
*/
Footer: ModalFooter,
});
export default Modal;
export { Modal, ModalBody, ModalFooter, ModalHeader };
export type { ModalProps, ModalBodyProps, ModalFooterProps, ModalHeaderProps };
@@ -0,0 +1,30 @@
import React, { useEffect } from "react";
function useIsModalOpen(modalRef: HTMLDialogElement | null) {
const [isOpen, setIsOpen] = React.useState<boolean>(false);
useEffect(() => {
if (!modalRef) {
return;
}
setIsOpen(modalRef.open); // eslint-disable-line react-hooks/set-state-in-effect
const observer = new MutationObserver(() => {
setIsOpen(modalRef.open);
});
observer.observe(modalRef, {
attributes: true,
attributeFilter: ["open"],
});
return () => {
observer.disconnect();
};
}, [modalRef]);
return isOpen;
}
export { useIsModalOpen };
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import React, { useState } from "react";
import { renderStoriesForChromatic } from "../utils/renderStoriesForChromatic";
import Pagination from "./Pagination";
import { Pagination } from "./index";
const meta: Meta<typeof Pagination> = {
title: "ds-react/Pagination",
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { getSteps } from "./Pagination";
import { getSteps } from "./get-steps";
describe("getSteps", () => {
test("lists all pages when count is <= 7", () => {
@@ -0,0 +1,41 @@
type PaginationStepsOptions = {
page: number;
count: number;
boundaryCount?: number;
siblingCount?: number;
};
const getSteps = ({
page,
count,
boundaryCount = 1,
siblingCount = 1,
}: PaginationStepsOptions) => {
const range = (start: number, end: number) =>
Array.from({ length: end - start + 1 }, (_, i) => start + i);
if (count <= (boundaryCount + siblingCount) * 2 + 3) return range(1, count);
const startPages = range(1, boundaryCount);
const endPages = range(count - boundaryCount + 1, count);
const siblingsStart = Math.max(
Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
boundaryCount + 2,
);
const siblingsEnd = siblingsStart + siblingCount * 2;
return [
...startPages,
siblingsStart - (startPages[startPages.length - 1] ?? 0) === 2
? siblingsStart - 1
: "ellipsis",
...range(siblingsStart, siblingsEnd),
(endPages[0] ?? count + 1) - siblingsEnd === 2
? siblingsEnd + 1
: "ellipsis",
...endPages,
];
};
export { getSteps };
+5 -5
View File
@@ -1,6 +1,6 @@
"use client";
export { default as Pagination, type PaginationProps } from "./Pagination";
export {
default as PaginationItem,
type PaginationItemProps,
} from "./PaginationItem";
export { Pagination, PaginationItem } from "./root/PaginationRoot";
export type {
PaginationProps,
PaginationItemProps,
} from "./root/PaginationRoot";
@@ -1,9 +1,9 @@
import React, { forwardRef } from "react";
import { Button, type ButtonProps } from "../button";
import type { OverridableComponent } from "../utils-external";
import { cl } from "../utils/helpers";
import { Button, type ButtonProps } from "../../button";
import type { OverridableComponent } from "../../utils-external";
import { cl } from "../../utils/helpers";
export interface PaginationItemProps extends ButtonProps {
interface PaginationItemProps extends ButtonProps {
children: React.ReactNode;
/**
* Sets selected styling if true
@@ -22,12 +22,12 @@ export interface PaginationItemProps extends ButtonProps {
size?: "medium" | "small" | "xsmall";
}
export type PaginationItemType = OverridableComponent<
type PaginationItemType = OverridableComponent<
PaginationItemProps,
HTMLAnchorElement
>;
export const Item: PaginationItemType = forwardRef(
const PaginationItem: PaginationItemType = forwardRef(
(
{
children,
@@ -63,4 +63,5 @@ export const Item: PaginationItemType = forwardRef(
},
);
export default Item;
export { PaginationItem };
export type { PaginationItemProps };
@@ -1,15 +1,16 @@
import React, { forwardRef } from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
import type { AkselStatusColorRole } from "@navikt/ds-tokens/types";
import type { AkselColor } from "../types";
import { BodyShort, Heading } from "../typography";
import { useId } from "../utils-external";
import { cl } from "../utils/helpers";
import { useI18n } from "../utils/i18n/i18n.hooks";
import PaginationItem, {
import type { AkselColor } from "../../types";
import { BodyShort, Heading } from "../../typography";
import { useId } from "../../utils-external";
import { cl } from "../../utils/helpers";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import { getSteps } from "../helpers/get-steps";
import {
PaginationItem,
type PaginationItemProps,
type PaginationItemType,
} from "./PaginationItem";
} from "../item/PaginationItem";
interface RenderItemProps extends Pick<
PaginationItemProps,
@@ -21,7 +22,7 @@ interface RenderItemProps extends Pick<
size: Exclude<PaginationProps["size"], undefined>;
}
export interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
/**
* Current page.
*
@@ -80,66 +81,7 @@ export interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
"data-color"?: Exclude<AkselColor, AkselStatusColorRole>;
}
interface PaginationType extends React.ForwardRefExoticComponent<
PaginationProps & React.RefAttributes<HTMLElement>
> {
Item: PaginationItemType;
}
export const getSteps = ({
page,
count,
boundaryCount = 1,
siblingCount = 1,
}: Pick<
PaginationProps,
"page" | "count" | "boundaryCount" | "siblingCount"
>) => {
const range = (start: number, end: number) =>
Array.from({ length: end - start + 1 }, (_, i) => start + i);
if (count <= (boundaryCount + siblingCount) * 2 + 3) return range(1, count);
const startPages = range(1, boundaryCount);
const endPages = range(count - boundaryCount + 1, count);
const siblingsStart = Math.max(
Math.min(page - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
boundaryCount + 2,
);
const siblingsEnd = siblingsStart + siblingCount * 2;
return [
...startPages,
siblingsStart - (startPages[startPages.length - 1] ?? 0) === 2
? siblingsStart - 1
: "ellipsis",
...range(siblingsStart, siblingsEnd),
(endPages[0] ?? count + 1) - siblingsEnd === 2
? siblingsEnd + 1
: "ellipsis",
...endPages,
];
};
/**
* A component that displays pagination controls.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/pagination)
* @see 🏷️ {@link PaginationProps}
*
* @example
* ```jsx
* <Pagination
* page={pageState}
* onPageChange={setPageState}
* count={9}
* boundaryCount={1}
* siblingCount={1}
* />
* ```
*/
export const Pagination = forwardRef<HTMLElement, PaginationProps>(
const PaginationRoot = forwardRef<HTMLElement, PaginationProps>(
(
{
page,
@@ -282,8 +224,31 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
</nav>
);
},
) as PaginationType;
);
Pagination.Item = PaginationItem;
/**
* A component that displays pagination controls.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/pagination)
* @see 🏷️ {@link PaginationProps}
*
* @example
* ```jsx
* <Pagination
* page={pageState}
* onPageChange={setPageState}
* count={9}
* boundaryCount={1}
* siblingCount={1}
* />
* ```
*/
const Pagination = Object.assign(PaginationRoot, {
/**
* @see 🏷️ {@link PaginationItemProps}
*/
Item: PaginationItem,
});
export default Pagination;
export { Pagination, PaginationItem };
export type { PaginationProps, PaginationItemProps };
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Button } from "../button";
import Popover from "./Popover";
import { Popover } from "./root/PopoverRoot";
const placements = [
"top",
@@ -1,7 +1,7 @@
import type { StoryObj } from "@storybook/react-vite";
import React, { useState } from "react";
import { expect, userEvent, waitFor, within } from "storybook/test";
import Popover from "./Popover";
import { Popover } from "./root/PopoverRoot";
export default {
title: "ds-react/Popover/tests",
@@ -1,11 +1,11 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
import { cl } from "../../utils/helpers";
export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export type PopoverContentType = React.ForwardRefExoticComponent<
type PopoverContentType = React.ForwardRefExoticComponent<
PopoverContentProps & React.RefAttributes<HTMLDivElement>
>;
@@ -21,4 +21,5 @@ const PopoverContent: PopoverContentType = forwardRef(
},
);
export default PopoverContent;
export { PopoverContent };
export type { PopoverContentProps, PopoverContentType };
+2 -5
View File
@@ -1,6 +1,3 @@
"use client";
export { default as Popover, type PopoverProps } from "./Popover";
export {
default as PopoverContent,
type PopoverContentProps,
} from "./PopoverContent";
export { Popover, PopoverContent } from "./root/PopoverRoot";
export type { PopoverProps, PopoverContentProps } from "./root/PopoverRoot";
@@ -9,14 +9,17 @@ import {
useFloating,
} from "@floating-ui/react";
import React, { type HTMLAttributes, forwardRef } from "react";
import { useModalContext } from "../modal/Modal.context";
import { omit, useClientLayoutEffect } from "../utils-external";
import { DismissableLayer } from "../utils/components/dismissablelayer/DismissableLayer";
import { cl } from "../utils/helpers";
import { useMergeRefs } from "../utils/hooks";
import PopoverContent, { type PopoverContentType } from "./PopoverContent";
import { useModalContext } from "../../modal/root/ModalRoot.context";
import { omit, useClientLayoutEffect } from "../../utils-external";
import { DismissableLayer } from "../../utils/components/dismissablelayer/DismissableLayer";
import { cl } from "../../utils/helpers";
import { useMergeRefs } from "../../utils/hooks";
import {
PopoverContent,
type PopoverContentProps,
} from "../content/PopoverContent";
export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
/**
* Popover content
*/
@@ -74,33 +77,7 @@ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
flip?: boolean;
}
interface PopoverComponent extends React.ForwardRefExoticComponent<
PopoverProps & React.RefAttributes<HTMLDivElement>
> {
Content: PopoverContentType;
}
/**
* A component that displays a popover.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/popover)
* @see 🏷️ {@link PopoverProps}
*
* @example
* ```jsx
* <Button ref={buttonRef} onClick={() => setOpenState(true)}>
* Åpne popover
* </Button>
* <Popover
* open={openState}
* onClose={() => setOpenState(false)}
* anchorEl={buttonRef.current}
* >
* <Popover.Content>Innhold her!</Popover.Content>
* </Popover>
* ```
*/
export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
const PopoverRoot = forwardRef<HTMLDivElement, PopoverProps>(
(
{
className,
@@ -186,7 +163,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
</DismissableLayer>
);
},
) as PopoverComponent;
);
const oppositeSideMap: Record<Side, Side> = {
top: "bottom",
@@ -214,6 +191,32 @@ function getOppositePlacement(placement: Placement): Placement[] {
return [alignment ? `${oppositeSide}-${alignment}` : oppositeSide];
}
Popover.Content = PopoverContent;
/**
* A component that displays a popover.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/popover)
* @see 🏷️ {@link PopoverProps}
*
* @example
* ```jsx
* <Button ref={buttonRef} onClick={() => setOpenState(true)}>
* Åpne popover
* </Button>
* <Popover
* open={openState}
* onClose={() => setOpenState(false)}
* anchorEl={buttonRef.current}
* >
* <Popover.Content>Innhold her!</Popover.Content>
* </Popover>
* ```
*/
const Popover = Object.assign(PopoverRoot, {
/**
* @see 🏷️ {@link PopoverContentProps}
*/
Content: PopoverContent,
});
export default Popover;
export { Popover, PopoverContent };
export type { PopoverProps, PopoverContentProps };
-16
View File
@@ -1,16 +0,0 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
export type BodyProps = React.HTMLAttributes<HTMLTableSectionElement>;
export type BodyType = React.ForwardRefExoticComponent<
BodyProps & React.RefAttributes<HTMLTableSectionElement>
>;
export const Body: BodyType = forwardRef(({ className, ...rest }, ref) => {
return (
<tbody {...rest} ref={ref} className={cl("aksel-table__body", className)} />
);
});
export default Body;
-20
View File
@@ -1,20 +0,0 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
export type HeaderProps = React.HTMLAttributes<HTMLTableSectionElement>;
export type HeaderType = React.ForwardRefExoticComponent<
HeaderProps & React.RefAttributes<HTMLTableSectionElement>
>;
export const Header: HeaderType = forwardRef(({ className, ...rest }, ref) => {
return (
<thead
{...rest}
ref={ref}
className={cl("aksel-table__header", className)}
/>
);
});
export default Header;
@@ -1,9 +1,9 @@
import React, { useState } from "react";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import { Button } from "../../../button";
import { Link } from "../../../link";
import { VStack } from "../../../primitives/stack";
import Table from "../../Table";
import { Button } from "../button";
import { Link } from "../link";
import { VStack } from "../primitives/stack";
import { Table } from "./index";
export default {
title: "ds-react/Table/Tests",
@@ -1,4 +1,6 @@
export interface SortState {
interface SortState {
orderBy: string;
direction: "ascending" | "descending" | "none";
}
export type { SortState };
@@ -1,10 +1,10 @@
/* https://github.com/Stanko/react-animate-height/blob/v3/src/index.tsx */
import React, { type CSSProperties, useEffect, useRef, useState } from "react";
import { useTimeout } from "../utils/hooks";
import { useTimeout } from "../../utils/hooks";
// ------------------ Types
export type Height = "auto" | number | `${number}%`;
type Height = "auto" | number | `${number}%`;
type Overflow = "auto" | "visible" | "hidden" | undefined;
// ------------------ Helpers
@@ -58,7 +58,7 @@ interface AnimateHeightProps extends React.HTMLAttributes<HTMLDivElement> {
innerClassName?: string;
}
const AnimateHeight: React.FC<AnimateHeightProps> = ({
const TableAnimateHeightInternal: React.FC<AnimateHeightProps> = ({
children,
className,
innerClassName,
@@ -251,4 +251,5 @@ const AnimateHeight: React.FC<AnimateHeightProps> = ({
);
};
export default AnimateHeight;
export { TableAnimateHeightInternal };
export type { Height };
@@ -0,0 +1,19 @@
import React, { forwardRef } from "react";
import { cl } from "../../utils/helpers";
type BodyProps = React.HTMLAttributes<HTMLTableSectionElement>;
const TableBody = forwardRef<HTMLTableSectionElement, BodyProps>(
({ className, ...rest }, ref) => {
return (
<tbody
{...rest}
ref={ref}
className={cl("aksel-table__body", className)}
/>
);
},
);
export { TableBody };
export type { BodyProps };
@@ -4,11 +4,14 @@ import {
SortDownIcon,
SortUpIcon,
} from "@navikt/aksel-icons";
import { consoleWarning } from "../utils/helpers/consoleWarning";
import HeaderCell, { type HeaderCellProps } from "./HeaderCell";
import { TableContext } from "./context";
import { consoleWarning } from "../../utils/helpers/consoleWarning";
import {
type HeaderCellProps,
TableHeaderCell,
} from "../header-cell/TableHeaderCell";
import { TableContext } from "../root/TableRoot.context";
export interface ColumnHeaderProps extends HeaderCellProps {
interface ColumnHeaderProps extends HeaderCellProps {
/**
* Key to sort by
*/
@@ -20,11 +23,7 @@ export interface ColumnHeaderProps extends HeaderCellProps {
sortable?: boolean;
}
export type ColumnHeaderType = React.ForwardRefExoticComponent<
ColumnHeaderProps & React.RefAttributes<HTMLTableCellElement>
>;
export const ColumnHeader: ColumnHeaderType = forwardRef(
const TableColumnHeader = forwardRef<HTMLTableCellElement, ColumnHeaderProps>(
({ children, sortable = false, sortKey, ...rest }, ref) => {
const context = useContext(TableContext);
@@ -33,7 +32,7 @@ export const ColumnHeader: ColumnHeaderType = forwardRef(
}
return (
<HeaderCell
<TableHeaderCell
scope="col"
ref={ref}
aria-sort={
@@ -70,9 +69,10 @@ export const ColumnHeader: ColumnHeaderType = forwardRef(
) : (
children
)}
</HeaderCell>
</TableHeaderCell>
);
},
);
export default ColumnHeader;
export { TableColumnHeader };
export type { ColumnHeaderProps };
-9
View File
@@ -1,9 +0,0 @@
import { createContext } from "react";
import type { SortState } from "./types";
export interface TableContextProps {
onSortChange?: (sortKey: string) => void;
sort?: SortState;
}
export const TableContext = createContext<TableContextProps | null>(null);
@@ -1,8 +1,8 @@
import React, { forwardRef } from "react";
import { BodyShort } from "../typography";
import { cl } from "../utils/helpers";
import { BodyShort } from "../../typography";
import { cl } from "../../utils/helpers";
export interface DataCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
interface DataCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
/**
* Content alignment inside cell
* @default "left"
@@ -14,7 +14,7 @@ export interface DataCellProps extends React.TdHTMLAttributes<HTMLTableCellEleme
textSize?: "medium" | "small";
}
export const DataCell = forwardRef<HTMLTableCellElement, DataCellProps>(
const TableDataCell = forwardRef<HTMLTableCellElement, DataCellProps>(
({ className, children = "", align, textSize, ...rest }, ref) => {
return (
<BodyShort
@@ -32,4 +32,5 @@ export const DataCell = forwardRef<HTMLTableCellElement, DataCellProps>(
},
);
export default DataCell;
export { TableDataCell };
export type { DataCellProps };
@@ -1,18 +1,15 @@
import React, { forwardRef } from "react";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { useId } from "../utils-external";
import { cl, composeEventHandlers } from "../utils/helpers";
import { useControllableState } from "../utils/hooks";
import { useI18n } from "../utils/i18n/i18n.hooks";
import AnimateHeight from "./AnimateHeight";
import DataCell from "./DataCell";
import Row, { type RowProps } from "./Row";
import { isElementInteractiveTarget } from "./Table.utils";
import { useId } from "../../utils-external";
import { cl, composeEventHandlers } from "../../utils/helpers";
import { useControllableState } from "../../utils/hooks";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import { TableAnimateHeightInternal } from "../animate-height/TableAnimateHeightInternal";
import { TableDataCell } from "../data-cell/TableDataCell";
import { isElementInteractiveTarget } from "../helpers/table-utils";
import { type RowProps, TableRow } from "../row/TableRow";
export interface ExpandableRowProps extends Omit<
RowProps,
"content" | "onRowClick"
> {
interface ExpandableRowProps extends Omit<RowProps, "content" | "onRowClick"> {
/**
* Content of the expanded row
*/
@@ -58,11 +55,7 @@ export interface ExpandableRowProps extends Omit<
contentGutter?: "left" | "right" | "none";
}
export type ExpandableRowType = React.ForwardRefExoticComponent<
ExpandableRowProps & React.RefAttributes<HTMLTableRowElement>
>;
export const ExpandableRow: ExpandableRowType = forwardRef(
const TableExpandableRow = forwardRef<HTMLTableRowElement, ExpandableRowProps>(
(
{
className,
@@ -104,7 +97,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
return (
<>
<Row
<TableRow
{...rest}
ref={ref}
className={cl("aksel-table__expandable-row", className, {
@@ -116,7 +109,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
onClick={composeEventHandlers(onClick, handleRowClick)}
>
{togglePlacement === "right" && children}
<DataCell
<TableDataCell
className={cl("aksel-table__toggle-expand-cell", {
"aksel-table__toggle-expand-cell--open": _open,
})}
@@ -136,9 +129,9 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
/>
</button>
)}
</DataCell>
</TableDataCell>
{togglePlacement === "left" && children}
</Row>
</TableRow>
<tr
data-state={_open ? "open" : "closed"}
className="aksel-table__expanded-row"
@@ -146,7 +139,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
id={id}
>
<td colSpan={colSpan} className="aksel-table__expanded-row-cell">
<AnimateHeight
<TableAnimateHeightInternal
className="aksel-table__expanded-row-collapse"
innerClassName={`aksel-table__expanded-row-content aksel-table__expanded-row-content--gutter-${
contentGutter ?? togglePlacement
@@ -156,7 +149,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
easing="cubic-bezier(0.39,0.57,0.56,1)"
>
{content}
</AnimateHeight>
</TableAnimateHeightInternal>
</td>
</tr>
</>
@@ -164,4 +157,5 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
},
);
export default ExpandableRow;
export { TableExpandableRow };
export type { ExpandableRowProps };
@@ -1,7 +1,7 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
import { cl } from "../../utils/helpers";
export interface HeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
interface HeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
scope?: string;
/**
* Content alignment inside cell
@@ -14,11 +14,7 @@ export interface HeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellEle
textSize?: "medium" | "small";
}
export type HeaderCellType = React.ForwardRefExoticComponent<
HeaderCellProps & React.RefAttributes<HTMLTableCellElement>
>;
export const HeaderCell: HeaderCellType = forwardRef(
const TableHeaderCell = forwardRef<HTMLTableCellElement, HeaderCellProps>(
({ className, children, align, textSize, ...rest }, ref) => {
return (
<th
@@ -35,4 +31,5 @@ export const HeaderCell: HeaderCellType = forwardRef(
},
);
export default HeaderCell;
export { TableHeaderCell };
export type { HeaderCellProps };
@@ -0,0 +1,19 @@
import React, { forwardRef } from "react";
import { cl } from "../../utils/helpers";
type HeaderProps = React.HTMLAttributes<HTMLTableSectionElement>;
const TableHeader = forwardRef<HTMLTableSectionElement, HeaderProps>(
({ className, ...rest }, ref) => {
return (
<thead
{...rest}
ref={ref}
className={cl("aksel-table__header", className)}
/>
);
},
);
export { TableHeader };
export type { HeaderProps };
+21 -14
View File
@@ -1,16 +1,23 @@
"use client";
export { default as TableBody, type BodyProps } from "./Body";
export {
default as TableColumnHeader,
type ColumnHeaderProps,
} from "./ColumnHeader";
export { default as TableDataCell, type DataCellProps } from "./DataCell";
export {
default as TableExpandableRow,
type ExpandableRowProps,
} from "./ExpandableRow";
export { default as TableHeader, type HeaderProps } from "./Header";
export { default as TableHeaderCell, type HeaderCellProps } from "./HeaderCell";
export { default as TableRow, type RowProps } from "./Row";
export { default as Table, type TableProps } from "./Table";
export type { SortState } from "./types";
Table,
TableBody,
TableColumnHeader,
TableDataCell,
TableExpandableRow,
TableHeader,
TableHeaderCell,
TableRow,
} from "./root/TableRoot";
export type {
BodyProps,
ColumnHeaderProps,
DataCellProps,
ExpandableRowProps,
HeaderCellProps,
HeaderProps,
RowProps,
SortState,
TableProps,
} from "./root/TableRoot";
@@ -0,0 +1,12 @@
import { createContext } from "react";
import type { SortState } from "../Table.types";
interface TableContextProps {
onSortChange?: (sortKey: string) => void;
sort?: SortState;
}
const TableContext = createContext<TableContextProps | null>(null);
export { TableContext };
export type { TableContextProps };
@@ -1,16 +1,25 @@
import React, { forwardRef } from "react";
import { cl } from "../utils/helpers";
import Body from "./Body";
import ColumnHeader from "./ColumnHeader";
import DataCell from "./DataCell";
import ExpandableRow from "./ExpandableRow";
import Header from "./Header";
import HeaderCell from "./HeaderCell";
import Row from "./Row";
import { TableContext } from "./context";
import type { SortState } from "./types";
import { cl } from "../../utils/helpers";
import type { SortState } from "../Table.types";
import { type BodyProps, TableBody } from "../body/TableBody";
import {
type ColumnHeaderProps,
TableColumnHeader,
} from "../column-header/TableColumnHeader";
import { type DataCellProps, TableDataCell } from "../data-cell/TableDataCell";
import {
type ExpandableRowProps,
TableExpandableRow,
} from "../expandable-row/TableExpandableRow";
import {
type HeaderCellProps,
TableHeaderCell,
} from "../header-cell/TableHeaderCell";
import { type HeaderProps, TableHeader } from "../header/TableHeader";
import { type RowProps, TableRow } from "../row/TableRow";
import { TableContext } from "./TableRoot.context";
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
/**
* Changes padding around Cells
* @default "medium"
@@ -36,17 +45,33 @@ export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement>
onSortChange?: (sortKey: string) => void;
}
export interface TableType extends React.ForwardRefExoticComponent<
TableProps & React.RefAttributes<HTMLTableElement>
> {
Header: typeof Header;
Body: typeof Body;
Row: typeof Row;
DataCell: typeof DataCell;
HeaderCell: typeof HeaderCell;
ColumnHeader: typeof ColumnHeader;
ExpandableRow: typeof ExpandableRow;
}
const TableRoot = forwardRef<HTMLTableElement, TableProps>(
(
{
className,
zebraStripes = false,
size = "medium",
onSortChange,
sort,
stickyHeader = false,
...rest
},
ref,
) => {
return (
<TableContext.Provider value={{ onSortChange, sort }}>
<table
{...rest}
ref={ref}
className={cl("aksel-table", `aksel-table--${size}`, className, {
"aksel-table--zebra-stripes": zebraStripes,
"aksel-table--sticky-header": stickyHeader,
})}
/>
</TableContext.Provider>
);
},
);
/**
* A component that displays a table with headers and rows.
@@ -80,40 +105,55 @@ export interface TableType extends React.ForwardRefExoticComponent<
* </Table>
* ```
*/
export const Table = forwardRef(
(
{
className,
zebraStripes = false,
size = "medium",
onSortChange,
sort,
stickyHeader = false,
...rest
},
ref,
) => {
return (
<TableContext.Provider value={{ onSortChange, sort }}>
<table
{...rest}
ref={ref}
className={cl("aksel-table", `aksel-table--${size}`, className, {
"aksel-table--zebra-stripes": zebraStripes,
"aksel-table--sticky-header": stickyHeader,
})}
/>
</TableContext.Provider>
);
},
) as TableType;
const Table = Object.assign(TableRoot, {
/**
* @see 🏷️ {@link HeaderProps}
*/
Header: TableHeader,
/**
* @see 🏷️ {@link BodyProps}
*/
Body: TableBody,
/**
* @see 🏷️ {@link RowProps}
*/
Row: TableRow,
/**
* @see 🏷️ {@link ColumnHeaderProps}
*/
ColumnHeader: TableColumnHeader,
/**
* @see 🏷️ {@link HeaderCellProps}
*/
HeaderCell: TableHeaderCell,
/**
* @see 🏷️ {@link DataCellProps}
*/
DataCell: TableDataCell,
/**
* @see 🏷️ {@link ExpandableRowProps}
*/
ExpandableRow: TableExpandableRow,
});
Table.Header = Header;
Table.Body = Body;
Table.Row = Row;
Table.ColumnHeader = ColumnHeader;
Table.HeaderCell = HeaderCell;
Table.DataCell = DataCell;
Table.ExpandableRow = ExpandableRow;
export default Table;
export {
Table,
TableBody,
TableColumnHeader,
TableDataCell,
TableExpandableRow,
TableHeader,
TableHeaderCell,
TableRow,
};
export type {
BodyProps,
ColumnHeaderProps,
DataCellProps,
ExpandableRowProps,
HeaderCellProps,
HeaderProps,
RowProps,
SortState,
TableProps,
};
@@ -1,8 +1,8 @@
import React, { forwardRef } from "react";
import { cl, composeEventHandlers } from "../utils/helpers";
import { isElementInteractiveTarget } from "./Table.utils";
import { cl, composeEventHandlers } from "../../utils/helpers";
import { isElementInteractiveTarget } from "../helpers/table-utils";
export interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
/**
* Row is selected
* @default false
@@ -22,11 +22,7 @@ export interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
onRowClick?: (event: React.MouseEvent<HTMLTableRowElement>) => void;
}
export type RowType = React.ForwardRefExoticComponent<
RowProps & React.RefAttributes<HTMLTableRowElement>
>;
export const Row: RowType = forwardRef(
const TableRow = forwardRef<HTMLTableRowElement, RowProps>(
(
{
className,
@@ -63,4 +59,5 @@ export const Row: RowType = forwardRef(
},
);
export default Row;
export { TableRow };
export type { RowProps };
@@ -3,7 +3,7 @@ import { Button } from "../../button";
import { Checkbox, CheckboxGroup } from "../../form/checkbox";
import { Radio, RadioGroup } from "../../form/radio";
import { Link } from "../../link";
import Table from "../Table";
import { Table } from "../index";
export default {
title: "ds-react/Table",
@@ -2,8 +2,8 @@ import React, { useState } from "react";
import useSWR from "swr";
import { Loader } from "../../loader";
import { Pagination } from "../../pagination";
import Table from "../Table";
import type { SortState } from "../types";
import type { SortState } from "../Table.types";
import { Table } from "../index";
import peopleJson from "./people.json";
export default {
@@ -6,7 +6,6 @@ import {
TimelineRow,
TimelineZoomButton,
} from "./index";
import { Zoom } from "./zoom";
const metadata: ComponentMetadata = {
name: "Timeline",
@@ -15,7 +14,6 @@ const metadata: ComponentMetadata = {
"Timeline.Row": TimelineRow,
"Timeline.Period": TimelinePeriod,
"Timeline.Pin": TimelinePin,
"Timeline.Zoom": Zoom,
"Timeline.ZoomButton": TimelineZoomButton,
},
keywords: ["timeline", "tidslinje", "gantt", "schedule", "periods"],
@@ -11,8 +11,8 @@ import { VStack } from "../primitives/stack";
import { Provider } from "../provider";
import en from "../utils/i18n/locales/en";
import { renderStoriesForChromatic } from "../utils/renderStoriesForChromatic";
import Timeline, { type TimelineProps } from "./Timeline";
import type { PeriodProps } from "./period/types";
import type { PeriodProps } from "./Timeline.types";
import { Timeline, type TimelineProps } from "./index";
export default {
title: "ds-react/Timeline",
@@ -1,7 +1,7 @@
import type { StoryObj } from "@storybook/react-vite";
import React from "react";
import { expect, userEvent } from "storybook/test";
import Timeline from "./Timeline";
import { Timeline } from "./index";
export default {
title: "ds-react/Timeline/Tests",
@@ -1,6 +1,6 @@
import type { TimelinePeriodProps } from ".";
import type { TimelinePeriodProps } from "./period/TimelinePeriod";
export interface PeriodProps {
interface PeriodProps {
start: Date;
end: Date;
status: Exclude<TimelinePeriodProps["status"], undefined>;
@@ -13,3 +13,5 @@ export interface PeriodProps {
statusLabel?: string;
restProps?: any;
}
export type { PeriodProps };
@@ -15,15 +15,15 @@ import {
subDays,
} from "date-fns";
import React, { useCallback, useMemo } from "react";
import { Detail } from "../typography/Detail";
import { useDateLocale, useI18n } from "../utils/i18n/i18n.hooks";
import type { TFunction } from "../utils/i18n/i18n.types";
import { useTimelineContext } from "./hooks/useTimelineContext";
import { isVisible } from "./utils";
import { horizontalPositionAndWidth } from "./utils/calc";
import type { AxisLabel, AxisLabelTemplates } from "./utils/types.external";
import { Detail } from "../../typography/Detail";
import { useDateLocale, useI18n } from "../../utils/i18n/i18n.hooks";
import type { TFunction } from "../../utils/i18n/i18n.types";
import { useTimelineContext } from "../root/TimelineRoot.context";
import { isVisible } from "../utils";
import { horizontalPositionAndWidth } from "../utils/calc";
import type { AxisLabel, AxisLabelTemplates } from "../utils/types.external";
export const dayLabels = (
const dayLabels = (
start: Date,
end: Date,
totalDays: number,
@@ -52,7 +52,7 @@ export const dayLabels = (
});
};
export const monthLabels = (
const monthLabels = (
start: Date,
end: Date,
direction: "left" | "right",
@@ -80,7 +80,7 @@ export const monthLabels = (
});
};
export const yearLabels = (
const yearLabels = (
start: Date,
end: Date,
direction: "left" | "right",
@@ -136,7 +136,7 @@ const getLabels = (
return yearLabels(start, end, direction, yearTemplate, locale);
};
export const AxisLabels = ({
const TimelineAxisLabelsInternal = ({
templates,
}: {
templates?: AxisLabelTemplates;
@@ -188,3 +188,5 @@ export const AxisLabels = ({
</div>
);
};
export { TimelineAxisLabelsInternal };
+14 -7
View File
@@ -1,9 +1,16 @@
"use client";
export { default as Timeline, type TimelineProps } from "./Timeline";
export { default as TimelineRow, type TimelineRowProps } from "./TimelineRow";
export { default as TimelinePin, type TimelinePinProps } from "./pin/Pin";
export { default as TimelinePeriod, type TimelinePeriodProps } from "./period";
export {
default as TimelineZoomButton,
type TimelineZoomButtonProps,
} from "./zoom/ZoomButton";
Timeline,
TimelinePeriod,
TimelinePin,
TimelineRow,
TimelineZoomButton,
} from "./root/TimelineRoot";
export type {
TimelinePeriodProps,
TimelinePinProps,
TimelineProps,
TimelineRowProps,
TimelineZoomButtonProps,
} from "./root/TimelineRoot";
@@ -16,8 +16,8 @@ import React, { useState } from "react";
import { cl } from "../../utils/helpers";
import { useMergeRefs } from "../../utils/hooks";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import type { PeriodProps } from "../Timeline.types";
import { ariaLabel, getConditionalClasses } from "../utils/period";
import type { PeriodProps } from "./types";
interface TimelineClickablePeriodProps extends PeriodProps {
onSelectPeriod?: (
@@ -32,7 +32,7 @@ interface TimelineClickablePeriodProps extends PeriodProps {
placement?: "top" | "bottom";
}
const ClickablePeriod = React.memo(
const TimelineClickablePeriodInternal = React.memo(
({
onSelectPeriod,
start,
@@ -161,4 +161,4 @@ const ClickablePeriod = React.memo(
},
);
export default ClickablePeriod;
export { TimelineClickablePeriodInternal };
@@ -1,14 +1,14 @@
import React from "react";
import { cl } from "../../utils/helpers";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import type { PeriodProps } from "../Timeline.types";
import { ariaLabel, getConditionalClasses } from "../utils/period";
import type { PeriodProps } from "./types";
interface TimelineNonClickablePeriodProps extends PeriodProps {
periodRef?: React.ForwardedRef<HTMLDivElement>;
}
const NonClickablePeriod = ({
const TimelineNonClickablePeriodInternal = ({
start,
end,
status,
@@ -57,4 +57,4 @@ const NonClickablePeriod = ({
);
};
export default NonClickablePeriod;
export { TimelineNonClickablePeriodInternal };
@@ -6,11 +6,11 @@ interface PeriodContextProps {
restProps?: any;
}
export const PeriodContext = createContext<PeriodContextProps>({
const PeriodContext = createContext<PeriodContextProps>({
periodId: "",
});
export const usePeriodContext = () => {
const usePeriodContext = () => {
const context = useContext(PeriodContext);
if (!context) {
@@ -21,3 +21,5 @@ export const usePeriodContext = () => {
return context;
};
export { PeriodContext, usePeriodContext };
@@ -0,0 +1,128 @@
import React, { forwardRef } from "react";
import type { AkselColor } from "../../types";
import { TimelineClickablePeriodInternal } from "../period-clickable/TimelineClickablePeriodInternal";
import { TimelineNonClickablePeriodInternal } from "../period-non-clickable/TimelineNonClickablePeriodInternal";
import { useRowContext } from "../row/TimelineRow.context";
import { usePeriodContext } from "./TimelinePeriod.context";
interface TimelinePeriodProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Period start date.
*/
start: Date;
/**
* Period end date.
*/
end: Date;
/**
* Icon for easier visual identification.
*/
icon?: React.ReactNode;
/**
* Period status.
* @default "neutral"
*/
status?: "success" | "warning" | "danger" | "info" | "neutral";
/**
* Overrides color set by status.
*
* @see 🏷️ {@link AkselColor}
* @see [📝 Documentation](https://aksel.nav.no/grunnleggende/styling/farger-tokens)
*/
"data-color"?: AkselColor;
/**
* Status label for screen-readers
* e.g "Sykemeldt", "foreldrepermisjon"
*/
statusLabel?: string;
/**
* Callback when selecting a period.
*/
onSelectPeriod?: (
event: React.MouseEvent<Element> | React.KeyboardEvent<Element>,
) => void;
/**
* Content displayed in Popover on click.
*/
children?: React.ReactNode;
/**
* Visual active inidcation on period.
*
* Make sure only one period is active at a time.
*/
isActive?: boolean;
/**
* Default orientation of popover
* @default "top"
*/
placement?: "top" | "bottom";
}
const TimelinePeriodBase = forwardRef<
HTMLDivElement | HTMLButtonElement,
TimelinePeriodProps
>(({ icon }: TimelinePeriodProps, ref) => {
const { periods } = useRowContext();
const { periodId, restProps } = usePeriodContext();
const period = periods.find((p) => p.id === periodId);
if (!period) {
return null;
}
const {
start,
endInclusive,
width,
horizontalPosition,
status = "neutral",
onSelectPeriod,
cropped,
direction,
children,
isActive,
statusLabel,
} = period;
return onSelectPeriod || children ? (
<TimelineClickablePeriodInternal
periodRef={ref as React.ForwardedRef<HTMLButtonElement>}
start={start}
end={endInclusive}
status={status}
onSelectPeriod={onSelectPeriod}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
isActive={isActive}
statusLabel={statusLabel}
restProps={restProps}
>
{children}
</TimelineClickablePeriodInternal>
) : (
<TimelineNonClickablePeriodInternal
periodRef={ref as React.ForwardedRef<HTMLDivElement>}
start={start}
end={endInclusive}
status={status}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
statusLabel={statusLabel}
restProps={restProps}
/>
);
});
const TimelinePeriod = Object.assign(TimelinePeriodBase, {
componentType: "period" as const,
});
export { TimelinePeriod };
export type { TimelinePeriodProps };
@@ -1,131 +0,0 @@
import React, { forwardRef } from "react";
import type { AkselColor } from "../../types";
import { usePeriodContext } from "../hooks/usePeriodContext";
import { useRowContext } from "../hooks/useRowContext";
import type { TimelineComponentTypes } from "../utils/types.internal";
import ClickablePeriod from "./ClickablePeriod";
import NonClickablePeriod from "./NonClickablePeriod";
export interface TimelinePeriodProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Period start date.
*/
start: Date;
/**
* Period end date.
*/
end: Date;
/**
* Icon for easier visual identification.
*/
icon?: React.ReactNode;
/**
* Period status.
* @default "neutral"
*/
status?: "success" | "warning" | "danger" | "info" | "neutral";
/**
* Overrides color set by status.
*
* @see 🏷️ {@link AkselColor}
* @see [📝 Documentation](https://aksel.nav.no/grunnleggende/styling/farger-tokens)
*/
"data-color"?: AkselColor;
/**
* Status label for screen-readers
* e.g "Sykemeldt", "foreldrepermisjon"
*/
statusLabel?: string;
/**
* Callback when selecting a period.
*/
onSelectPeriod?: (
event: React.MouseEvent<Element> | React.KeyboardEvent<Element>,
) => void;
/**
* Content displayed in Popover on click.
*/
children?: React.ReactNode;
/**
* Visual active inidcation on period.
*
* Make sure only one period is active at a time.
*/
isActive?: boolean;
/**
* Default orientation of popover
* @default "top"
*/
placement?: "top" | "bottom";
}
export interface PeriodType extends React.ForwardRefExoticComponent<
TimelinePeriodProps & React.RefAttributes<HTMLDivElement | HTMLButtonElement>
> {
componentType: TimelineComponentTypes;
}
export const Period = forwardRef<HTMLDivElement, TimelinePeriodProps>(
({ icon }: TimelinePeriodProps, ref) => {
const { periods } = useRowContext();
const { periodId, restProps } = usePeriodContext();
const period = periods.find((p) => p.id === periodId);
if (!period) {
return null;
}
const {
start,
endInclusive,
width,
horizontalPosition,
status = "neutral",
onSelectPeriod,
cropped,
direction,
children,
isActive,
statusLabel,
} = period;
return onSelectPeriod || children ? (
<ClickablePeriod
periodRef={ref as React.ForwardedRef<HTMLButtonElement>}
start={start}
end={endInclusive}
status={status}
onSelectPeriod={onSelectPeriod}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
isActive={isActive}
statusLabel={statusLabel}
restProps={restProps}
>
{children}
</ClickablePeriod>
) : (
<NonClickablePeriod
periodRef={ref}
start={start}
end={endInclusive}
status={status}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
statusLabel={statusLabel}
restProps={restProps}
/>
);
},
) as PeriodType;
Period.componentType = "period";
export default Period;
@@ -1,32 +0,0 @@
import { isBefore } from "date-fns";
import React, { forwardRef } from "react";
import { useTimelineContext } from "../hooks/useTimelineContext";
import type { TimelineComponentTypes } from "../utils/types.internal";
import PinInternal, { type TimelinePinProps } from "./PinInternal";
export interface PinType extends React.ForwardRefExoticComponent<
TimelinePinProps & React.RefAttributes<HTMLButtonElement>
> {
componentType: TimelineComponentTypes;
}
export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
({ date, ...restProps }, forwardedRef) => {
const { startDate, endDate } = useTimelineContext();
/**
* Out-of-bounds pins should not be rendered
*/
if (isBefore(date, startDate) || isBefore(endDate, date)) {
return null;
}
return <PinInternal ref={forwardedRef} {...restProps} date={date} />;
},
) as PinType;
Pin.componentType = "pin";
export default Pin;
export type { TimelinePinProps };
@@ -0,0 +1,31 @@
import { isBefore } from "date-fns";
import React, { forwardRef } from "react";
import { useTimelineContext } from "../root/TimelineRoot.context";
import {
TimelinePinInternal,
type TimelinePinProps,
} from "./TimelinePinInternal";
const TimelinePinBase = forwardRef<HTMLButtonElement, TimelinePinProps>(
({ date, ...restProps }, forwardedRef) => {
const { startDate, endDate } = useTimelineContext();
/**
* Out-of-bounds pins should not be rendered
*/
if (isBefore(date, startDate) || isBefore(endDate, date)) {
return null;
}
return (
<TimelinePinInternal ref={forwardedRef} {...restProps} date={date} />
);
},
);
const TimelinePin = Object.assign(TimelinePinBase, {
componentType: "pin" as const,
});
export { TimelinePin };
export type { TimelinePinProps };
@@ -16,11 +16,11 @@ import { format } from "date-fns";
import React, { forwardRef, useMemo, useState } from "react";
import { useMergeRefs } from "../../utils/hooks";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import { useTimelineKeyboardContext } from "../hooks/TimelineKeyboardNavProvider";
import { useTimelineContext } from "../hooks/useTimelineContext";
import { useTimelineKeyboardContext } from "../root/TimelineKeyboardNavProvider";
import { useTimelineContext } from "../root/TimelineRoot.context";
import { position } from "../utils/calc";
export interface TimelinePinProps extends React.HTMLAttributes<HTMLButtonElement> {
interface TimelinePinProps extends React.HTMLAttributes<HTMLButtonElement> {
/**
* Date position for the pin.
*/
@@ -31,7 +31,7 @@ export interface TimelinePinProps extends React.HTMLAttributes<HTMLButtonElement
children?: React.ReactNode;
}
export const PinInternal = forwardRef<HTMLButtonElement, TimelinePinProps>(
const TimelinePinInternal = forwardRef<HTMLButtonElement, TimelinePinProps>(
({ date, children, ...rest }, ref) => {
const { startDate, endDate, direction } = useTimelineContext();
const { handlePinKeyDown } = useTimelineKeyboardContext();
@@ -127,4 +127,5 @@ export const PinInternal = forwardRef<HTMLButtonElement, TimelinePinProps>(
},
);
export default PinInternal;
export { TimelinePinInternal };
export type { TimelinePinProps };
@@ -9,7 +9,7 @@ interface TimelineContextProps {
setEndInclusive: (d: Date) => void;
}
export const TimelineContext = createContext<TimelineContextProps>({
const TimelineContext = createContext<TimelineContextProps>({
startDate: new Date(),
endDate: new Date(),
direction: "left",
@@ -17,7 +17,7 @@ export const TimelineContext = createContext<TimelineContextProps>({
setEndInclusive: () => null,
});
export const useTimelineContext = () => {
const useTimelineContext = () => {
const context = useContext(TimelineContext);
if (!context) {
@@ -28,3 +28,5 @@ export const useTimelineContext = () => {
return context;
};
export { TimelineContext, useTimelineContext };
@@ -1,23 +1,30 @@
import { endOfDay, isSameDay, startOfDay } from "date-fns";
import React, { forwardRef, useMemo, useState } from "react";
import { consoleWarning } from "../utils/helpers/consoleWarning";
import { AxisLabels } from "./AxisLabels";
import TimelineRow, { type TimelineRowType } from "./TimelineRow";
import { TimelineKeyboardNavProvider } from "./hooks/TimelineKeyboardNavProvider";
import { RowContext } from "./hooks/useRowContext";
import { TimelineContext } from "./hooks/useTimelineContext";
import { consoleWarning } from "../../utils/helpers/consoleWarning";
import { TimelineAxisLabelsInternal } from "../axis-labels/TimelineAxisLabelsInternal";
import {
TimelinePeriod,
type TimelinePeriodProps,
} from "../period/TimelinePeriod";
import { TimelinePin, type TimelinePinProps } from "../pin/TimelinePin";
import { TimelineRow, type TimelineRowProps } from "../row/TimelineRow";
import { RowContext } from "../row/TimelineRow.context";
import { parseRows } from "../utils/timeline";
import type { AxisLabelTemplates } from "../utils/types.external";
import {
TimelineZoomButton,
type TimelineZoomButtonProps,
} from "../zoom-button/TimelineZoomButton";
import { TimelineZoom, type TimelineZoomProps } from "../zoom/TimelineZoom";
import { TimelineKeyboardNavProvider } from "./TimelineKeyboardNavProvider";
import { TimelineContext } from "./TimelineRoot.context";
import {
useEarliestDate,
useLatestDate,
useTimelineRows,
} from "./hooks/useTimelineRows";
import Period, { type PeriodType } from "./period";
import Pin, { type PinType } from "./pin/Pin";
import { parseRows } from "./utils/timeline";
import type { AxisLabelTemplates } from "./utils/types.external";
import Zoom, { type ZoomType } from "./zoom";
} from "./useTimelineRows";
export interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* Decides startingpoint in timeline.
@@ -46,45 +53,7 @@ export interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
axisLabelTemplates?: AxisLabelTemplates;
}
interface TimelineComponent extends React.ForwardRefExoticComponent<TimelineProps> {
/**
* @see 🏷️ {@link TimelineRowType}
*/
Row: TimelineRowType;
/**
* @see 🏷️ {@link PeriodType}
*/
Period: PeriodType;
/**
* @see 🏷️ {@link PinType}
*/
Pin: PinType;
/**
* @see 🏷️ {@link ZoomType}
*/
Zoom: ZoomType;
}
/**
* A component that displays a timeline of events. Meant for Internal systems.
*
* Component is made for desktop enviroments and will start having issues on smaller screens.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/timeline)
* @see 🏷️ {@link TimelineProps}
*
* @example
* ```jsx
* <Timeline>
* <Timeline.Row>
* <Timeline.Period start={new Date("2020-01-01")} end={new Date("2020-01-31")}>
* <p>Period 1</p>
* </Timeline.Period>
* <Timeline.Row>
* </Timeline>
* ```
*/
export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
const TimelineRoot = forwardRef<HTMLDivElement, TimelineProps>(
(
{
children,
@@ -186,7 +155,7 @@ export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
<TimelineKeyboardNavProvider>
<div {...rest} ref={forwardedRef}>
<div className="aksel-timeline">
<AxisLabels templates={axisLabelTemplates} />
<TimelineAxisLabelsInternal templates={axisLabelTemplates} />
{pins.map(({ key, PinChild }) => (
<PinChild key={key} />
@@ -219,11 +188,58 @@ export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
</TimelineContext.Provider>
);
},
) as TimelineComponent;
);
Timeline.Row = TimelineRow;
Timeline.Period = Period;
Timeline.Pin = Pin;
Timeline.Zoom = Zoom;
/**
* A component that displays a timeline of events. Meant for Internal systems.
*
* Component is made for desktop enviroments and will start having issues on smaller screens.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/timeline)
* @see 🏷️ {@link TimelineProps}
*
* @example
* ```jsx
* <Timeline>
* <Timeline.Row>
* <Timeline.Period start={new Date("2020-01-01")} end={new Date("2020-01-31")}>
* <p>Period 1</p>
* </Timeline.Period>
* <Timeline.Row>
* </Timeline>
* ```
*/
const Timeline = Object.assign(TimelineRoot, {
/**
* @see 🏷️ {@link TimelineRowProps}
*/
Row: TimelineRow,
/**
* @see 🏷️ {@link TimelinePeriodProps}
*/
Period: TimelinePeriod,
/**
* @see 🏷️ {@link TimelinePinProps}
*/
Pin: TimelinePin,
/**
* @see 🏷️ {@link TimelineZoomProps}
*/
Zoom: TimelineZoom,
});
export default Timeline;
export {
Timeline,
TimelinePeriod,
TimelinePin,
TimelineRow,
TimelineZoomButton,
};
export type {
TimelinePeriodProps,
TimelinePinProps,
TimelineProps,
TimelineRowProps,
TimelineZoomButtonProps,
TimelineZoomProps,
};
@@ -5,7 +5,7 @@ import {
useEarliestDate,
useLatestDate,
useTimelineRows,
} from "../hooks/useTimelineRows";
} from "./useTimelineRows";
describe("useEarliestDate", () => {
test("returns the provided startDate if it exists", () => {
@@ -92,7 +92,7 @@ const trimmedPeriods = (period: PositionedPeriod) => {
};
};
export const useTimelineRows = (
const useTimelineRows = (
rows: any,
startDate: Date,
endDate: Date,
@@ -134,7 +134,7 @@ export const useTimelineRows = (
[rows, startDate, endDate, direction],
);
export const useEarliestDate = ({
const useEarliestDate = ({
startDate,
rows,
}: {
@@ -168,8 +168,10 @@ const latestDate = (latest: Date, period: Period) =>
const latestTomDate = (rows: Period[][]) =>
rows.flat().reduce(latestDate, new Date(0));
export const useLatestDate = ({ endDate, rows }: any) =>
const useLatestDate = ({ endDate, rows }: any) =>
useMemo(
() => (endDate ? endDate : addDays(latestTomDate(rows), 1)),
[endDate, rows],
);
export { useTimelineRows, useEarliestDate, useLatestDate };
@@ -8,13 +8,13 @@ interface RowContextProps {
index: number;
}
export const RowContext = createContext<RowContextProps>({
const RowContext = createContext<RowContextProps>({
periods: [],
id: "",
index: 0,
});
export const useRowContext = () => {
const useRowContext = () => {
const context = useContext(RowContext);
if (!context) {
@@ -23,3 +23,5 @@ export const useRowContext = () => {
return context;
};
export { RowContext, useRowContext };
@@ -1,23 +1,20 @@
import { format } from "date-fns";
import React, { forwardRef } from "react";
import { BodyShort } from "../typography/BodyShort";
import { cl } from "../utils/helpers";
import { useI18n } from "../utils/i18n/i18n.hooks";
import { BodyShort } from "../../typography/BodyShort";
import { cl } from "../../utils/helpers";
import { useI18n } from "../../utils/i18n/i18n.hooks";
import { TimelinePeriod } from "../period/TimelinePeriod";
import { PeriodContext } from "../period/TimelinePeriod.context";
import {
useTimelineKeyboardActiveRow,
useTimelineKeyboardContext,
} from "./hooks/TimelineKeyboardNavProvider";
import { PeriodContext } from "./hooks/usePeriodContext";
import { useRowContext } from "./hooks/useRowContext";
import Period from "./period";
import type {
PositionedPeriod,
TimelineComponentTypes,
} from "./utils/types.internal";
} from "../root/TimelineKeyboardNavProvider";
import type { PositionedPeriod } from "../utils/types.internal";
import { useRowContext } from "./TimelineRow.context";
type TimelineRowBaseProps = React.HTMLAttributes<HTMLOListElement>;
export type TimelineRowProps = TimelineRowBaseProps &
type TimelineRowProps = TimelineRowBaseProps &
(
| {
/**
@@ -44,13 +41,7 @@ export type TimelineRowProps = TimelineRowBaseProps &
}
);
export interface TimelineRowType extends React.ForwardRefExoticComponent<
TimelineRowProps & React.RefAttributes<HTMLOListElement>
> {
componentType: TimelineComponentTypes;
}
export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
const TimelineRowBase = forwardRef<HTMLOListElement, TimelineRowProps>(
({ label, className, headingTag = "h3", icon, ...rest }, ref) => {
const { periods } = useRowContext();
const { updateActiveRow, handleRowKeyDown } = useTimelineKeyboardContext();
@@ -117,7 +108,7 @@ export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
restProps: period?.restProps,
}}
>
<Period
<TimelinePeriod
start={period.start}
end={period.endInclusive}
icon={period.icon}
@@ -132,8 +123,11 @@ export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
</>
);
},
) as TimelineRowType;
);
TimelineRow.componentType = "row";
const TimelineRow = Object.assign(TimelineRowBase, {
componentType: "row" as const,
});
export default TimelineRow;
export { TimelineRow };
export type { TimelineRowProps };
@@ -1,11 +1,11 @@
import { differenceInMilliseconds } from "date-fns";
export const position = (date: Date, start: Date, endInclusive: Date) => {
const position = (date: Date, start: Date, endInclusive: Date) => {
const diff = differenceInMilliseconds(endInclusive, start);
return (differenceInMilliseconds(date, start) / diff) * 100;
};
export const horizontalPositionAndWidth = (
const horizontalPositionAndWidth = (
start: Date,
endInclusive: Date,
timelineStart: Date,
@@ -24,3 +24,5 @@ export const horizontalPositionAndWidth = (
width,
};
};
export { position, horizontalPositionAndWidth };
@@ -1,33 +1,36 @@
import { differenceInDays } from "date-fns";
import type { PeriodProps } from "../period/types";
import type { PeriodProps } from "../Timeline.types";
import type { Spatial } from "./types.internal";
export interface Positioned {
interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export const isVisible = ({ horizontalPosition }: Positioned): boolean => {
const isVisible = ({ horizontalPosition }: Positioned): boolean => {
return horizontalPosition <= 96 && horizontalPosition >= 0;
};
export const getFirstDate = (periods: any) => {
const getFirstDate = (periods: any) => {
return periods.sort(
(a: PeriodProps, b: PeriodProps) => a.start.getTime() - b.start.getTime(),
)[0].start;
};
export const getLastDate = (periods: any) => {
const getLastDate = (periods: any) => {
return periods.sort(
(a: PeriodProps, b: PeriodProps) => a.end.getTime() - b.end.getTime(),
)[periods.length - 1].end;
};
export const withinADay = (date1: Date, date2: Date): boolean =>
const withinADay = (date1: Date, date2: Date): boolean =>
differenceInDays(date1, date2) <= 1;
export const invisiblePeriods = ({
const invisiblePeriods = ({
horizontalPosition,
width,
}: Positioned & Spatial) =>
horizontalPosition >= 0 && horizontalPosition <= 100 && width > 0;
export { isVisible, getFirstDate, getLastDate, withinADay, invisiblePeriods };
export type { Positioned };
@@ -1,9 +1,9 @@
import { format } from "date-fns";
import { cl } from "../../utils/helpers";
import type { TFunction } from "../../utils/i18n/i18n.types";
import type { PeriodProps } from "../period/types";
import type { PeriodProps } from "../Timeline.types";
export const getConditionalClasses = (
const getConditionalClasses = (
cropped: string,
direction: string,
status: PeriodProps["status"],
@@ -19,7 +19,7 @@ export const getConditionalClasses = (
});
};
export const ariaLabel = (
const ariaLabel = (
startDate: Date,
endDate: Date,
status: PeriodProps["status"],
@@ -35,3 +35,5 @@ export const ariaLabel = (
end,
});
};
export { getConditionalClasses, ariaLabel };
@@ -1,11 +1,12 @@
import { differenceInMilliseconds } from "date-fns";
import type { Period, PositionedPeriod } from "./types.internal";
export const lastDate = (a: Date, b: Date): number =>
differenceInMilliseconds(a, b);
const lastDate = (a: Date, b: Date): number => differenceInMilliseconds(a, b);
export const lastPeriod = (a: PositionedPeriod, b: PositionedPeriod): number =>
const lastPeriod = (a: PositionedPeriod, b: PositionedPeriod): number =>
a.horizontalPosition - b.horizontalPosition;
export const lastSinglePeriod = (a: Period, b: Period): number =>
const lastSinglePeriod = (a: Period, b: Period): number =>
differenceInMilliseconds(b.endInclusive, a.endInclusive);
export { lastDate, lastPeriod, lastSinglePeriod };
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more