diff --git a/.changeset/silly-shrimps-peel.md b/.changeset/silly-shrimps-peel.md new file mode 100644 index 0000000000..fd6ff38a5b --- /dev/null +++ b/.changeset/silly-shrimps-peel.md @@ -0,0 +1,6 @@ +--- +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +Primitives: Added support for padding, paddingInline, paddingBlock, margin, marginInline, marginBlock, width, minWidth, maxWidth, height, minHeight, maxHeight, position, inset, top, right, left, bottom, overflow, overflowX, overflowY, flexBasis, flexGrow, flexShrink to Box, HGrid and Stack. diff --git a/@navikt/core/css/primitives/base.css b/@navikt/core/css/primitives/base.css index ef70966708..8cef4648e8 100644 --- a/@navikt/core/css/primitives/base.css +++ b/@navikt/core/css/primitives/base.css @@ -17,9 +17,9 @@ --__ac-r-pi-lg: var(--__ac-r-pi-md); --__ac-r-pi-xl: var(--__ac-r-pi-lg); --__ac-r-pi-2xl: var(--__ac-r-pi-xl); - --__ac-r-pi: var(--__ac-r-p-xs); + --__ac-r-pi: var(--__ac-r-pi-xs); - padding-inline: var(--__ac-r-pi); + padding-inline: var(--__ac-r-pi, var(--__ac-r-padding)); } .navds-r-pb { @@ -29,9 +29,45 @@ --__ac-r-pb-lg: var(--__ac-r-pb-md); --__ac-r-pb-xl: var(--__ac-r-pb-lg); --__ac-r-pb-2xl: var(--__ac-r-pb-xl); - --__ac-r-pb: var(--__ac-r-p-xs); + --__ac-r-pb: var(--__ac-r-pb-xs); - padding-block: var(--__ac-r-pb); + padding-block: var(--__ac-r-pb, var(--__ac-r-padding)); +} + +.navds-r-m { + --__ac-r-m-xs: initial; + --__ac-r-m-sm: var(--__ac-r-m-xs); + --__ac-r-m-md: var(--__ac-r-m-sm); + --__ac-r-m-lg: var(--__ac-r-m-md); + --__ac-r-m-xl: var(--__ac-r-m-lg); + --__ac-r-m-2xl: var(--__ac-r-m-xl); + --__ac-r-margin: var(--__ac-r-m-xs); + + margin: var(--__ac-r-margin); +} + +.navds-r-mi { + --__ac-r-mi-xs: initial; + --__ac-r-mi-sm: var(--__ac-r-mi-xs); + --__ac-r-mi-md: var(--__ac-r-mi-sm); + --__ac-r-mi-lg: var(--__ac-r-mi-md); + --__ac-r-mi-xl: var(--__ac-r-mi-lg); + --__ac-r-mi-2xl: var(--__ac-r-mi-xl); + --__ac-r-mi: var(--__ac-r-mi-xs); + + margin-inline: var(--__ac-r-mi, var(--__ac-r-margin)); +} + +.navds-r-mb { + --__ac-r-mb-xs: initial; + --__ac-r-mb-sm: var(--__ac-r-mb-xs); + --__ac-r-mb-md: var(--__ac-r-mb-sm); + --__ac-r-mb-lg: var(--__ac-r-mb-md); + --__ac-r-mb-xl: var(--__ac-r-mb-lg); + --__ac-r-mb-2xl: var(--__ac-r-mb-xl); + --__ac-r-mb: var(--__ac-r-mb-xs); + + margin-block: var(--__ac-r-mb, var(--__ac-r-margin)); } .navds-r-w { @@ -263,6 +299,18 @@ --__ac-r-pb: var(--__ac-r-pb-sm); } + .navds-r-m { + --__ac-r-margin: var(--__ac-r-m-sm); + } + + .navds-r-mi { + --__ac-r-mi: var(--__ac-r-mi-sm); + } + + .navds-r-mb { + --__ac-r-mb: var(--__ac-r-mb-sm); + } + .navds-r-w { --__ac-r-w: var(--__ac-r-w-sm); } @@ -349,6 +397,18 @@ --__ac-r-pb: var(--__ac-r-pb-md); } + .navds-r-m { + --__ac-r-margin: var(--__ac-r-m-md); + } + + .navds-r-mi { + --__ac-r-mi: var(--__ac-r-mi-md); + } + + .navds-r-mb { + --__ac-r-mb: var(--__ac-r-mb-md); + } + .navds-r-w { --__ac-r-w: var(--__ac-r-w-md); } @@ -435,6 +495,18 @@ --__ac-r-pb: var(--__ac-r-pb-lg); } + .navds-r-m { + --__ac-r-margin: var(--__ac-r-m-lg); + } + + .navds-r-mi { + --__ac-r-mi: var(--__ac-r-mi-lg); + } + + .navds-r-mb { + --__ac-r-mb: var(--__ac-r-mb-lg); + } + .navds-r-w { --__ac-r-w: var(--__ac-r-w-lg); } @@ -521,6 +593,18 @@ --__ac-r-pb: var(--__ac-r-pb-xl); } + .navds-r-m { + --__ac-r-margin: var(--__ac-r-m-xl); + } + + .navds-r-mi { + --__ac-r-mi: var(--__ac-r-mi-xl); + } + + .navds-r-mb { + --__ac-r-mb: var(--__ac-r-mb-xl); + } + .navds-r-w { --__ac-r-w: var(--__ac-r-w-xl); } @@ -607,6 +691,18 @@ --__ac-r-pb: var(--__ac-r-pb-2xl); } + .navds-r-m { + --__ac-r-margin: var(--__ac-r-m-2xl); + } + + .navds-r-mi { + --__ac-r-mi: var(--__ac-r-mi-2xl); + } + + .navds-r-mb { + --__ac-r-mb: var(--__ac-r-mb-2xl); + } + .navds-r-w { --__ac-r-w: var(--__ac-r-w-2xl); } diff --git a/@navikt/core/css/primitives/box.css b/@navikt/core/css/primitives/box.css index bbd8f09d63..93579e7fdf 100644 --- a/@navikt/core/css/primitives/box.css +++ b/@navikt/core/css/primitives/box.css @@ -1,29 +1,23 @@ -.navds-box { - --__ac-box-padding-xs: initial; - --__ac-box-padding-sm: var(--__ac-box-padding-xs); - --__ac-box-padding-md: var(--__ac-box-padding-sm); - --__ac-box-padding-lg: var(--__ac-box-padding-md); - --__ac-box-padding-xl: var(--__ac-box-padding-lg); - --__ac-box-padding-2xl: var(--__ac-box-padding-xl); - --__ac-box-padding-inline-xs: initial; - --__ac-box-padding-inline-sm: var(--__ac-box-padding-inline-xs); - --__ac-box-padding-inline-md: var(--__ac-box-padding-inline-sm); - --__ac-box-padding-inline-lg: var(--__ac-box-padding-inline-md); - --__ac-box-padding-inline-xl: var(--__ac-box-padding-inline-lg); - --__ac-box-padding-inline-2xl: var(--__ac-box-padding-inline-xl); - --__ac-box-padding-block-xs: initial; - --__ac-box-padding-block-sm: var(--__ac-box-padding-block-xs); - --__ac-box-padding-block-md: var(--__ac-box-padding-block-sm); - --__ac-box-padding-block-lg: var(--__ac-box-padding-block-md); - --__ac-box-padding-block-xl: var(--__ac-box-padding-block-lg); - --__ac-box-padding-block-2xl: var(--__ac-box-padding-block-xl); - --__ac-box-padding: var(--__ac-box-padding-xs); - --__ac-box-padding-inline: var(--__ac-box-padding-inline-xs); - --__ac-box-padding-block: var(--__ac-box-padding-block-xs); +.navds-box-bg { --__ac-box-background: initial; + + background-color: var(--__ac-box-background); +} + +.navds-box-border-color { --__ac-box-border-color: initial; - --__ac-box-shadow: initial; + + border-color: var(--__ac-box-border-color); +} + +.navds-box-border-width { --__ac-box-border-width: initial; + + border-style: solid; + border-width: var(--__ac-box-border-width, 0); +} + +.navds-box-border-radius { --__ac-box-border-radius-xs: initial; --__ac-box-border-radius-sm: var(--__ac-box-border-radius-xs); --__ac-box-border-radius-md: var(--__ac-box-border-radius-sm); @@ -32,57 +26,41 @@ --__ac-box-border-radius-2xl: var(--__ac-box-border-radius-xl); --__ac-box-border-radius: var(--__ac-box-border-radius-xs); - padding-inline: var(--__ac-box-padding-inline, var(--__ac-box-padding)); - padding-block: var(--__ac-box-padding-block, var(--__ac-box-padding)); - background-color: var(--__ac-box-background); - border-style: solid; - border-color: var(--__ac-box-border-color); border-radius: var(--__ac-box-border-radius); +} + +.navds-box-shadow { + --__ac-box-shadow: initial; + box-shadow: var(--__ac-box-shadow); - border-width: var(--__ac-box-border-width, 0); } @media (min-width: 480px) { - .navds-box { - --__ac-box-padding: var(--__ac-box-padding-sm); - --__ac-box-padding-inline: var(--__ac-box-padding-inline-sm); - --__ac-box-padding-block: var(--__ac-box-padding-block-sm); + .navds-box-border-radius { --__ac-box-border-radius: var(--__ac-box-border-radius-sm); } } @media (min-width: 768px) { - .navds-box { - --__ac-box-padding: var(--__ac-box-padding-md); - --__ac-box-padding-inline: var(--__ac-box-padding-inline-md); - --__ac-box-padding-block: var(--__ac-box-padding-block-md); + .navds-box-border-radius { --__ac-box-border-radius: var(--__ac-box-border-radius-md); } } @media (min-width: 1024px) { - .navds-box { - --__ac-box-padding: var(--__ac-box-padding-lg); - --__ac-box-padding-inline: var(--__ac-box-padding-inline-lg); - --__ac-box-padding-block: var(--__ac-box-padding-block-lg); + .navds-box-border-radius { --__ac-box-border-radius: var(--__ac-box-border-radius-lg); } } @media (min-width: 1280px) { - .navds-box { - --__ac-box-padding: var(--__ac-box-padding-xl); - --__ac-box-padding-inline: var(--__ac-box-padding-inline-xl); - --__ac-box-padding-block: var(--__ac-box-padding-block-xl); + .navds-box-border-radius { --__ac-box-border-radius: var(--__ac-box-border-radius-xl); } } @media (min-width: 1440px) { - .navds-box { - --__ac-box-padding: var(--__ac-box-padding-2xl); - --__ac-box-padding-inline: var(--__ac-box-padding-inline-2xl); - --__ac-box-padding-block: var(--__ac-box-padding-block-2xl); + .navds-box-border-radius { --__ac-box-border-radius: var(--__ac-box-border-radius-2xl); } } diff --git a/@navikt/core/react/src/layout/base/BasePrimitive.stories.tsx b/@navikt/core/react/src/layout/base/BasePrimitive.stories.tsx index 2842ba9cdf..f59c2673b9 100644 --- a/@navikt/core/react/src/layout/base/BasePrimitive.stories.tsx +++ b/@navikt/core/react/src/layout/base/BasePrimitive.stories.tsx @@ -85,6 +85,56 @@ export const Padding = () => ( ); +export const Margin = () => ( + + + + Margin all around + + + + + Margin to the North + + + + + Margin to the East + + + + + Margin to the South + + + + + Margin to the West + + + +); + +export const MarginAuto = () => ( + + + + MarginInline: auto + + + + + MarginInline: auto 0 + + + + + MarginInline: 0 auto + + + +); + export const HeightWidth = () => ( @@ -216,6 +266,15 @@ export const Chromatic: Story = {

Padding

+
+

Margin

+ +
+
+

MarginAuto

+ +
+

Height & Width

diff --git a/@navikt/core/react/src/layout/base/BasePrimitive.tsx b/@navikt/core/react/src/layout/base/BasePrimitive.tsx index b15af3fb14..dfd741c4d1 100644 --- a/@navikt/core/react/src/layout/base/BasePrimitive.tsx +++ b/@navikt/core/react/src/layout/base/BasePrimitive.tsx @@ -4,8 +4,7 @@ import { Slot } from "../../slot/Slot"; import { getResponsiveProps, getResponsiveValue } from "../utilities/css"; import { ResponsiveProp, SpacingScale } from "../utilities/types"; -export interface BasePrimitiveProps { - children: React.ReactElement; +export type PrimitiveProps = { className?: string; /** * Padding around children. @@ -40,6 +39,47 @@ export interface BasePrimitiveProps { paddingBlock?: ResponsiveProp< SpacingScale | `${SpacingScale} ${SpacingScale}` >; + /** + * Margin around element. + * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) + * or an object of spacing tokens for different breakpoints. + * @example + * margin='4' + * margin={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}} + */ + margin?: ResponsiveProp; + /** + * Horizontal margin around element. + * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) + * or an object of spacing tokens for different breakpoints. + * @example + * marginInline='4' + * marginInline='4 5' + * marginInline={{xs: '0 32', sm: '3', md: '4 5', lg: '5', xl: '6'}} + */ + marginInline?: ResponsiveProp< + | SpacingScale + | `${SpacingScale} ${SpacingScale}` + | "auto" + | `auto ${SpacingScale}` + | `${SpacingScale} auto` + >; + /** + * Vertical margin around element. + * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) + * or an object of spacing tokens for different breakpoints. + * @example + * marginBlock='4' + * marginBlock='4 5' + * marginBlock={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}} + */ + marginBlock?: ResponsiveProp< + | SpacingScale + | `${SpacingScale} ${SpacingScale}` + | "auto" + | `auto ${SpacingScale}` + | `${SpacingScale} auto` + >; /** * CSS `width` */ @@ -140,6 +180,38 @@ export interface BasePrimitiveProps { * CSS `flex-grow` */ flexGrow?: ResponsiveProp; +}; + +export const PRIMITIVE_PROPS: (keyof PrimitiveProps)[] = [ + "className", + "padding", + "paddingInline", + "paddingBlock", + "margin", + "marginInline", + "marginBlock", + "width", + "minWidth", + "maxWidth", + "height", + "minHeight", + "maxHeight", + "position", + "inset", + "top", + "right", + "bottom", + "left", + "overflow", + "overflowX", + "overflowY", + "flexBasis", + "flexGrow", + "flexShrink", +]; + +interface BasePrimitiveProps extends PrimitiveProps { + children: React.ReactElement; } export const BasePrimitive = ({ @@ -148,6 +220,9 @@ export const BasePrimitive = ({ padding, paddingInline, paddingBlock, + margin, + marginInline, + marginBlock, width, minWidth, maxWidth, @@ -172,6 +247,10 @@ export const BasePrimitive = ({ ...getResponsiveProps("r", "p", "spacing", padding), ...getResponsiveProps("r", "pi", "spacing", paddingInline), ...getResponsiveProps("r", "pb", "spacing", paddingBlock), + /* Margin */ + ...getResponsiveProps("r", "m", "spacing", margin), + ...getResponsiveProps("r", "mi", "spacing", marginInline), + ...getResponsiveProps("r", "mb", "spacing", marginBlock), /* Width & height */ ...getResponsiveValue("r", "w", width), ...getResponsiveValue("r", "minw", minWidth), @@ -203,6 +282,9 @@ export const BasePrimitive = ({ "navds-r-p": padding, "navds-r-pi": paddingInline, "navds-r-pb": paddingBlock, + "navds-r-m": margin, + "navds-r-mi": marginInline, + "navds-r-mb": marginBlock, "navds-r-w": width, "navds-r-minw": minWidth, "navds-r-maxw": maxWidth, diff --git a/@navikt/core/react/src/layout/base/PrimitiveAsChildProps.ts b/@navikt/core/react/src/layout/base/PrimitiveAsChildProps.ts new file mode 100644 index 0000000000..b931a3c7fb --- /dev/null +++ b/@navikt/core/react/src/layout/base/PrimitiveAsChildProps.ts @@ -0,0 +1,42 @@ +export type PrimitiveAsChildProps = + | { + children: React.ReactElement | false | null; + /** + * Renders the component and its child as a single element, + * merging the props of the component with the props of the child. + * @example + * ```tsx + * + * + * + * + * // Renders + *
+ * ``` + */ + asChild: true; + /** + * Implements [OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) + * + * When using asChild, the prop is not allowed as it would have no effect. + */ + as?: never; + } + | { + children?: React.ReactNode; + /** + * Renders the component and its child as a single element, + * merging the props of the component with the props of the child. + * + * @example + * ```tsx + * + * + * + * + * // Renders + *
+ * ``` + */ + asChild?: false; + }; diff --git a/@navikt/core/react/src/layout/box/Box.stories.tsx b/@navikt/core/react/src/layout/box/Box.stories.tsx index 297c863487..d54374564a 100644 --- a/@navikt/core/react/src/layout/box/Box.stories.tsx +++ b/@navikt/core/react/src/layout/box/Box.stories.tsx @@ -368,11 +368,25 @@ export const BorderRadius = () => ( ); +export const MarginDemo = () => ( + + + + Box + + + +); + export const PaddingDemo = () => ( Box @@ -380,6 +394,14 @@ export const PaddingDemo = () => ( ); +export const AsChild = () => ( + + + + + +); + export const Chromatic: Story = { render: () => ( diff --git a/@navikt/core/react/src/layout/box/Box.tsx b/@navikt/core/react/src/layout/box/Box.tsx index 0f36ad992a..2d2c70128e 100644 --- a/@navikt/core/react/src/layout/box/Box.tsx +++ b/@navikt/core/react/src/layout/box/Box.tsx @@ -1,6 +1,13 @@ import cl from "clsx"; import React, { forwardRef } from "react"; +import { Slot } from "../../slot/Slot"; +import { omit } from "../../util"; import { OverridableComponent } from "../../util/types"; +import BasePrimitive, { + PRIMITIVE_PROPS, + PrimitiveProps, +} from "../base/BasePrimitive"; +import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps"; import { getResponsiveProps } from "../utilities/css"; import { BackgroundColorToken, @@ -9,77 +16,45 @@ import { ResponsiveProp, ShadowToken, SpaceDelimitedAttribute, - SpacingScale, SurfaceColorToken, } from "../utilities/types"; -export interface BoxProps extends React.HTMLAttributes { - /** - * CSS `background-color` property. - * Accepts a [background/surface color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#afff774dad80). - */ - background?: BackgroundColorToken | SurfaceColorToken; - /** - * CSS `border-color` property. - * Accepts a [border color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#adb1767e2f87). - */ - borderColor?: BorderColorToken; - /** - * CSS `border-radius` property. - * Accepts a [radius token](https://aksel.nav.no/grunnleggende/styling/design-tokens#6d79c5605d31) - * or an object of radius tokens for different breakpoints. - * @example - * borderRadius='full' - * borderRadius='0 full large small' - * borderRadius={{xs: 'small large', sm: '0', md: 'large', lg: 'full'}} - */ - borderRadius?: ResponsiveProp>; - /** - * CSS `border-width` property. If this is not set there will be no border. - * @example - * borderWidth='2' - * borderWidth='1 2 3 4' - */ - borderWidth?: SpaceDelimitedAttribute<"0" | "1" | "2" | "3" | "4" | "5">; - /** - * Padding around children. - * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) - * or an object of spacing tokens for different breakpoints. - * @example - * padding='4' - * padding={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}} - */ - padding?: ResponsiveProp; - /** - * Horizontal padding around children. - * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) - * or an object of spacing tokens for different breakpoints. - * @example - * paddingInline='4' - * paddingInline='4 5' - * paddingInline={{xs: '0 32', sm: '3', md: '4 5', lg: '5', xl: '6'}} - */ - paddingInline?: ResponsiveProp< - SpacingScale | `${SpacingScale} ${SpacingScale}` - >; - /** - * Vertical padding around children. - * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) - * or an object of spacing tokens for different breakpoints. - * @example - * paddingBlock='4' - * paddingBlock='4 5' - * paddingBlock={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}} - */ - paddingBlock?: ResponsiveProp< - SpacingScale | `${SpacingScale} ${SpacingScale}` - >; - /** Shadow on box. Accepts a shadow token. - * @example - * shadow='small' - */ - shadow?: ShadowToken; -} +export type BoxProps = PrimitiveProps & + PrimitiveAsChildProps & + React.HTMLAttributes & { + /** + * CSS `background-color` property. + * Accepts a [background/surface color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#afff774dad80). + */ + background?: BackgroundColorToken | SurfaceColorToken; + /** + * CSS `border-color` property. + * Accepts a [border color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#adb1767e2f87). + */ + borderColor?: BorderColorToken; + /** + * CSS `border-radius` property. + * Accepts a [radius token](https://aksel.nav.no/grunnleggende/styling/design-tokens#6d79c5605d31) + * or an object of radius tokens for different breakpoints. + * @example + * borderRadius='full' + * borderRadius='0 full large small' + * borderRadius={{xs: 'small large', sm: '0', md: 'large', lg: 'full'}} + */ + borderRadius?: ResponsiveProp>; + /** + * CSS `border-width` property. If this is not set there will be no border. + * @example + * borderWidth='2' + * borderWidth='1 2 3 4' + */ + borderWidth?: SpaceDelimitedAttribute<"0" | "1" | "2" | "3" | "4" | "5">; + /** Shadow on box. Accepts a shadow token. + * @example + * shadow='small' + */ + shadow?: ShadowToken; + }; /** * Foundational Layout-primitive for generic encapsulation & styling. @@ -111,17 +86,16 @@ export interface BoxProps extends React.HTMLAttributes { export const Box: OverridableComponent = forwardRef( ( { + children, + className, as: Component = "div", background, borderColor, borderWidth, borderRadius, - className, - padding, - paddingInline, - paddingBlock, shadow, style: _style, + asChild, ...rest }, ref, @@ -149,18 +123,27 @@ export const Box: OverridableComponent = forwardRef( false, ["0"], ), - ...getResponsiveProps("box", "padding", "spacing", padding), - ...getResponsiveProps("box", "padding-inline", "spacing", paddingInline), - ...getResponsiveProps("box", "padding-block", "spacing", paddingBlock), }; + const Comp = asChild ? Slot : Component; + return ( - + + + {children} + + ); }, ); diff --git a/@navikt/core/react/src/layout/grid/HGrid.tsx b/@navikt/core/react/src/layout/grid/HGrid.tsx index b878b97e42..fcfb697c6d 100644 --- a/@navikt/core/react/src/layout/grid/HGrid.tsx +++ b/@navikt/core/react/src/layout/grid/HGrid.tsx @@ -1,34 +1,42 @@ import cl from "clsx"; -import React, { HTMLAttributes, forwardRef } from "react"; +import React, { forwardRef } from "react"; +import { Slot } from "../../slot/Slot"; +import { OverridableComponent, omit } from "../../util"; +import BasePrimitive, { + PRIMITIVE_PROPS, + PrimitiveProps, +} from "../base/BasePrimitive"; +import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps"; import { getResponsiveProps, getResponsiveValue } from "../utilities/css"; import { ResponsiveProp, SpacingScale } from "../utilities/types"; -export interface HGridProps extends HTMLAttributes { - children: React.ReactNode; - /** - * Number of columns to display. Can be a number, a string, or an object with values for specific breakpoints. - * Sets `grid-template-columns`, so `fr`, `minmax` etc. works. - * @example - * columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}} - * columns={3} - * columns="repeat(3, minmax(0, 1fr))" - */ - columns?: ResponsiveProp; - /** - * Spacing between columns. - * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) - * or an object of spacing tokens for different breakpoints. - * @example - * gap="6" - * gap="8 4" - * gap={{ sm: "2", md: "2", lg: "6", xl: "6"}} - */ - gap?: ResponsiveProp; - /** - * Vertical alignment of children. Elements will by default stretch to the height of parent-element. - */ - align?: "start" | "center" | "end"; -} +export type HGridProps = PrimitiveProps & + PrimitiveAsChildProps & + React.HTMLAttributes & { + /** + * Number of columns to display. Can be a number, a string, or an object with values for specific breakpoints. + * Sets `grid-template-columns`, so `fr`, `minmax` etc. works. + * @example + * columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}} + * columns={3} + * columns="repeat(3, minmax(0, 1fr))" + */ + columns?: ResponsiveProp; + /** + * Spacing between columns. + * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) + * or an object of spacing tokens for different breakpoints. + * @example + * gap="6" + * gap="8 4" + * gap={{ sm: "2", md: "2", lg: "6", xl: "6"}} + */ + gap?: ResponsiveProp; + /** + * Vertical alignment of children. Elements will by default stretch to the height of parent-element. + */ + align?: "start" | "center" | "end"; + }; /** * Horizontal Grid Primitive with dynamic columns and gap based on breakpoints. * @@ -54,25 +62,45 @@ export interface HGridProps extends HTMLAttributes { *
* */ -export const HGrid = forwardRef( - ({ className, columns, gap, style, align, ...rest }, ref) => { - const styles: React.CSSProperties = { - ...style, - "--__ac-hgrid-align": align, - ...getResponsiveProps(`hgrid`, "gap", "spacing", gap), - ...getResponsiveValue(`hgrid`, "columns", formatGrid(columns)), - }; +export const HGrid: OverridableComponent = + forwardRef( + ( + { + children, + className, + as: Component = "div", + columns, + gap, + style, + align, + asChild, + ...rest + }, + ref, + ) => { + const styles: React.CSSProperties = { + ...style, + "--__ac-hgrid-align": align, + ...getResponsiveProps(`hgrid`, "gap", "spacing", gap), + ...getResponsiveValue(`hgrid`, "columns", formatGrid(columns)), + }; + + const Comp = asChild ? Slot : Component; - return ( -
- ); - }, -); + return ( + + + {children} + + + ); + }, + ); function formatGrid( props?: ResponsiveProp, diff --git a/@navikt/core/react/src/layout/stack/HStack.tsx b/@navikt/core/react/src/layout/stack/HStack.tsx index e5a0622817..c0826df69e 100644 --- a/@navikt/core/react/src/layout/stack/HStack.tsx +++ b/@navikt/core/react/src/layout/stack/HStack.tsx @@ -1,8 +1,9 @@ import React, { forwardRef } from "react"; import { OverridableComponent } from "../../util/types"; +import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps"; import { Stack, StackProps } from "./Stack"; -export type HStackProps = Omit; +export type HStackProps = PrimitiveAsChildProps & Omit; /** * Layout-primitive for horizontal flexbox diff --git a/@navikt/core/react/src/layout/stack/Stack.tsx b/@navikt/core/react/src/layout/stack/Stack.tsx index 59ad0b3d77..4bba9fe0f0 100644 --- a/@navikt/core/react/src/layout/stack/Stack.tsx +++ b/@navikt/core/react/src/layout/stack/Stack.tsx @@ -1,74 +1,84 @@ import cl from "clsx"; import React, { HTMLAttributes, forwardRef } from "react"; +import { Slot } from "../../slot/Slot"; +import { omit } from "../../util"; import { OverridableComponent } from "../../util/types"; +import BasePrimitive, { + PRIMITIVE_PROPS, + PrimitiveProps, +} from "../base/BasePrimitive"; +import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps"; import { getResponsiveProps, getResponsiveValue } from "../utilities/css"; import { ResponsiveProp, SpacingScale } from "../utilities/types"; -export interface StackProps extends HTMLAttributes { - children: React.ReactNode; - /** - * CSS `justify-content` property. - * - * @example - * justify='center' - * justify={{xs: 'start', sm: 'center', md: 'end', lg: 'space-around', xl: 'space-between'}} - */ - justify?: ResponsiveProp< - | "start" - | "center" - | "end" - | "space-around" - | "space-between" - | "space-evenly" - >; - /** - * CSS `align-items` property. - * - * @example - * align='center' - * align={{xs: 'start', sm: 'center', md: 'end', lg: 'baseline', xl: 'stretch'}} - */ - align?: ResponsiveProp<"start" | "center" | "end" | "baseline" | "stretch">; - /** - * Sets the CSS `flex-wrap` property. - */ - wrap?: boolean; - /** - * CSS `gap` property. - * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) - * or an object of spacing tokens for different breakpoints. - * - * @example - * gap="6" - * gap="8 4" - * gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}} - */ - gap?: ResponsiveProp; - /** - * CSS `flex-direction` property. - * @default "row" - * - * @example - * direction='row' - * direction={{xs: 'row', sm: 'column'}} - */ - direction?: ResponsiveProp< - "row" | "column" | "row-reverse" | "column-reverse" - >; -} +export type StackProps = PrimitiveProps & + PrimitiveAsChildProps & + HTMLAttributes & { + /** + * CSS `justify-content` property. + * + * @example + * justify='center' + * justify={{xs: 'start', sm: 'center', md: 'end', lg: 'space-around', xl: 'space-between'}} + */ + justify?: ResponsiveProp< + | "start" + | "center" + | "end" + | "space-around" + | "space-between" + | "space-evenly" + >; + /** + * CSS `align-items` property. + * + * @example + * align='center' + * align={{xs: 'start', sm: 'center', md: 'end', lg: 'baseline', xl: 'stretch'}} + */ + align?: ResponsiveProp<"start" | "center" | "end" | "baseline" | "stretch">; + /** + * Sets the CSS `flex-wrap` property. + */ + wrap?: boolean; + /** + * CSS `gap` property. + * Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213) + * or an object of spacing tokens for different breakpoints. + * + * @example + * gap='4' + * gap='8 4' + * gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}} + */ + gap?: ResponsiveProp; + /** + * CSS `flex-direction` property. + * @default "row" + * + * @example + * direction='row' + * direction={{xs: 'row', sm: 'column'}} + */ + direction?: ResponsiveProp< + "row" | "column" | "row-reverse" | "column-reverse" + >; + }; export const Stack: OverridableComponent = forwardRef( ( { - as: Component = "div", + children, className, + as: Component = "div", align, justify, wrap = true, gap, style: _style, direction = "row", + asChild, ...rest }, ref, @@ -82,16 +92,22 @@ export const Stack: OverridableComponent = ...getResponsiveValue(`stack`, "justify", justify), }; + const Comp = asChild ? Slot : Component; + return ( - + + + {children} + + ); }, ); diff --git a/@navikt/core/react/src/layout/stack/VStack.tsx b/@navikt/core/react/src/layout/stack/VStack.tsx index 5e4150fa83..5c06682591 100644 --- a/@navikt/core/react/src/layout/stack/VStack.tsx +++ b/@navikt/core/react/src/layout/stack/VStack.tsx @@ -1,8 +1,10 @@ import React, { forwardRef } from "react"; import { OverridableComponent } from "../../util/types"; +import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps"; import { Stack, StackProps } from "./Stack"; -export type VStackProps = Omit; +export type VStackProps = PrimitiveAsChildProps & + Omit; /** * Layout-primitive for vetical flexbox diff --git a/@navikt/core/react/src/layout/utilities/css.ts b/@navikt/core/react/src/layout/utilities/css.ts index c558839ec4..55dfd72cda 100644 --- a/@navikt/core/react/src/layout/utilities/css.ts +++ b/@navikt/core/react/src/layout/utilities/css.ts @@ -50,6 +50,9 @@ const translateTokenStringToCSS = ( const width = 100 / arr.length; return `calc((100vw - ${width}%)/2)`; } + if (["mi", "mb"].includes(componentProp) && x === "auto") { + return "auto"; + } let output = `var(--a-${tokenSubgroup}-${x})`; if (tokenExceptions.includes(x)) { diff --git a/@navikt/core/react/src/util/types/AsChildProps.ts b/@navikt/core/react/src/util/types/AsChildProps.ts index b291dbb12a..549175f1fa 100644 --- a/@navikt/core/react/src/util/types/AsChildProps.ts +++ b/@navikt/core/react/src/util/types/AsChildProps.ts @@ -16,6 +16,7 @@ export type AsChildProps = * ``` */ asChild: true; + as?: never; } | { children: React.ReactNode; diff --git a/biome.json b/biome.json index 922418e387..fd2b1b7c64 100644 --- a/biome.json +++ b/biome.json @@ -38,7 +38,7 @@ }, "suspicious": { "noExplicitAny": "off", - "noArrayIndexKey": "off", + "noArrayIndexKey": "off" }, "complexity": { "noForEach": "off", diff --git a/scripts/docgen.ts b/scripts/docgen.ts index 72ae9cb7a2..5389aa168b 100644 --- a/scripts/docgen.ts +++ b/scripts/docgen.ts @@ -7,6 +7,9 @@ const options: docgen.ParserOptions = { shouldRemoveUndefinedFromOptional: true, propFilter: (prop) => { + if (prop.name === "as" && prop.type.name === "undefined") { + return false; + } if (prop.name === "className" || prop.parent?.name === "RefAttributes") { return true; }