mirror of
https://github.com/navikt/aksel.git
synced 2026-10-21 21:22:03 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b89d3540e | ||
|
|
ea14000efd | ||
|
|
88c245e6b9 | ||
|
|
a45e9cc97b | ||
|
|
594373bde4 | ||
|
|
955589ca0e |
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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
+7
-6
@@ -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",
|
||||
});
|
||||
+7
-6
@@ -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 };
|
||||
+6
-5
@@ -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 };
|
||||
+66
-65
@@ -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,
|
||||
};
|
||||
+6
-5
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
+9
-7
@@ -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 };
|
||||
+4
-2
@@ -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 };
|
||||
+29
-34
@@ -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
-1
@@ -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 };
|
||||
+4
-3
@@ -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 };
|
||||
+4
-3
@@ -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 };
|
||||
+7
-6
@@ -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 };
|
||||
+6
-6
@@ -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 };
|
||||
@@ -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";
|
||||
+4
-2
@@ -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 };
|
||||
+2
-1
@@ -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);
|
||||
+78
-74
@@ -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
-1
@@ -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 };
|
||||
@@ -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";
|
||||
+8
-7
@@ -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 };
|
||||
+37
-72
@@ -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",
|
||||
|
||||
+5
-4
@@ -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 };
|
||||
@@ -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";
|
||||
+40
-37
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+4
-4
@@ -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 };
|
||||
+5
-4
@@ -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 };
|
||||
+12
-12
@@ -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 };
|
||||
@@ -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);
|
||||
+6
-5
@@ -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 };
|
||||
+18
-24
@@ -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 };
|
||||
+5
-8
@@ -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 };
|
||||
File renamed without changes.
@@ -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 };
|
||||
+98
-58
@@ -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,
|
||||
};
|
||||
+6
-9
@@ -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",
|
||||
|
||||
+4
-2
@@ -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 };
|
||||
+13
-11
@@ -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 };
|
||||
@@ -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";
|
||||
+3
-3
@@ -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 };
|
||||
+3
-3
@@ -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 };
|
||||
+4
-2
@@ -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 };
|
||||
+6
-5
@@ -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 };
|
||||
File renamed without changes.
+4
-2
@@ -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 };
|
||||
+75
-59
@@ -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,
|
||||
};
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
useEarliestDate,
|
||||
useLatestDate,
|
||||
useTimelineRows,
|
||||
} from "../hooks/useTimelineRows";
|
||||
} from "./useTimelineRows";
|
||||
|
||||
describe("useEarliestDate", () => {
|
||||
test("returns the provided startDate if it exists", () => {
|
||||
+5
-3
@@ -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 };
|
||||
+4
-2
@@ -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 };
|
||||
+17
-23
@@ -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
Reference in new issue
Block a user