-
- Velg språk
-
- Norsk
- Engelsk
- Spansk
- Fransk
-
+
+ Velg språk
+
+ Norsk
+ Engelsk
+ Spansk
+ Fransk
+
Velg språk for å endre innholdet på siden
-
+
diff --git a/packages/css/dropdown.css b/packages/css/dropdown.css
new file mode 100644
index 0000000000..f3881b085f
--- /dev/null
+++ b/packages/css/dropdown.css
@@ -0,0 +1,45 @@
+.ds-dropdown {
+ --dsc-dropdown-padding: var(--ds-spacing-3) var(--ds-spacing-2);
+ --dsc-dropdown-min-width: 16rem;
+ --dsc-dropdown-item-padding: 0 var(--ds-spacing-4);
+ --dsc-dropdown-header-padding: var(--ds-spacing-2) var(--ds-spacing-4);
+
+ padding: var(--dsc-dropdown-padding);
+ list-style: none;
+ border-radius: min(1rem, var(--ds-border-radius-md));
+ box-shadow: var(--ds-shadow-md);
+ background-color: var(--ds-color-neutral-background-default);
+ border: 1px solid var(--ds-color-neutral-border-subtle);
+ min-width: var(--dsc-dropdown-min-width);
+
+ /* Remove popover arrow */
+ &::before {
+ display: none;
+ }
+
+ &[data-size='sm'] {
+ --dsc-dropdown-padding: var(--ds-spacing-2);
+ --dsc-dropdown-min-width: 15rem;
+ }
+
+ &[data-size='lg'] {
+ --dsc-dropdown-padding: var(--ds-spacing-4) var(--ds-spacing-2);
+ --dsc-dropdown-min-width: 18rem;
+ }
+
+ & :is(a, button, [role='button']) {
+ justify-content: start;
+ padding: var(--dsc-dropdown-item-padding);
+ width: 100%;
+ }
+
+ .ds-dropdown__list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+
+ .ds-dropdown__heading {
+ padding: var(--dsc-dropdown-header-padding);
+ }
+}
diff --git a/packages/css/dropdownmenu.css b/packages/css/dropdownmenu.css
deleted file mode 100644
index 206d581e38..0000000000
--- a/packages/css/dropdownmenu.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.ds-dropdownmenu {
- --dsc-dropdownmenu-padding: var(--ds-spacing-3) var(--ds-spacing-2);
- --dsc-dropdownmenu-min-width: 16rem;
- --dsc-dropdownmenu-item-padding: 0 var(--ds-spacing-4);
- --dsc-dropdownmenu-header-padding: var(--ds-spacing-2) var(--ds-spacing-4);
-
- position: relative;
- padding: var(--dsc-dropdownmenu-padding);
- z-index: 1500;
- margin: 0;
- list-style: none;
- border-radius: min(1rem, var(--ds-border-radius-md));
- box-shadow: var(--ds-shadow-md);
- background-color: var(--ds-color-neutral-background-default);
- border: 1px solid var(--ds-color-neutral-border-subtle);
- min-width: var(--dsc-dropdownmenu-min-width);
-
- &[data-size='sm'] {
- --dsc-dropdownmenu-padding: var(--ds-spacing-2);
- --dsc-dropdownmenu-min-width: 15rem;
- }
-
- &[data-size='lg'] {
- --dsc-dropdownmenu-padding: var(--ds-spacing-4) var(--ds-spacing-2);
- --dsc-dropdownmenu-min-width: 18rem;
- }
-}
-
-.ds-dropdownmenu__item {
- justify-content: start;
- padding: var(--dsc-dropdownmenu-item-padding);
- width: 100%;
-}
-
-.ds-dropdownmenu__group {
- margin: 0;
- padding: 0;
- list-style: none;
-}
-
-.ds-dropdownmenu__heading {
- padding: var(--dsc-dropdownmenu-header-padding);
-}
diff --git a/packages/css/index.css b/packages/css/index.css
index f60c02ac0f..fef99b2e5f 100644
--- a/packages/css/index.css
+++ b/packages/css/index.css
@@ -12,6 +12,7 @@
@import url('./utilities.css') layer(ds.utilities);
@import url('./button.css') layer(ds.components);
@import url('./alert.css') layer(ds.components);
+@import url('./popover.css') layer(ds.components);
@import url('./skiplink.css') layer(ds.components);
@import url('./accordion.css') layer(ds.components);
@import url('./switch.css') layer(ds.components);
@@ -27,12 +28,11 @@
@import url('./card.css') layer(ds.components);
@import url('./link.css') layer(ds.components);
@import url('./fieldset.css') layer(ds.components);
-@import url('./dropdownmenu.css') layer(ds.components);
+@import url('./dropdown.css') layer(ds.components);
@import url('./chip') layer(ds.components);
@import url('./divider.css') layer(ds.components);
@import url('./tabs.css') layer(ds.components);
@import url('./pagination.css') layer(ds.components);
-@import url('./popover.css') layer(ds.components);
@import url('./skeleton.css') layer(ds.components);
@import url('./tag.css') layer(ds.components);
@import url('./error-summary.css') layer(ds.components);
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index bd1ba9ecaa..fcd334652a 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -529,7 +529,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
### Bug Fixes
-- **DropDownMenuItem:** add list style none ([#1190](https://github.com/digdir/designsystemet/issues/1190)) ([11bd19b](https://github.com/digdir/designsystemet/commit/11bd19bfb6ac76b2c697a22e876117c4128be3bd))
+- **DropdownMenuItem:** add list style none ([#1190](https://github.com/digdir/designsystemet/issues/1190)) ([11bd19b](https://github.com/digdir/designsystemet/commit/11bd19bfb6ac76b2c697a22e876117c4128be3bd))
- **List:** Wrap in `div` to allow access to `Heading` ([#1217](https://github.com/digdir/designsystemet/issues/1217)) ([afcadb7](https://github.com/digdir/designsystemet/commit/afcadb7c4cb4b368d247af0c41ed8debf53c4b66))
- **Pagination:** Only use needed space for buttons ([#1220](https://github.com/digdir/designsystemet/issues/1220)) ([4bf3d74](https://github.com/digdir/designsystemet/commit/4bf3d745888f500259df5aadf4edee97ec4f95bc))
- **Select:** Select not working properly in Modal ([#1195](https://github.com/digdir/designsystemet/issues/1195)) ([fb8be6a](https://github.com/digdir/designsystemet/commit/fb8be6a647ba0da8b5b23e65813508f34e09c8c1))
diff --git a/packages/react/src/components/Avatar/Avatar.stories.tsx b/packages/react/src/components/Avatar/Avatar.stories.tsx
index a6653f6cdb..bdceb4b65d 100644
--- a/packages/react/src/components/Avatar/Avatar.stories.tsx
+++ b/packages/react/src/components/Avatar/Avatar.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryFn } from '@storybook/react';
import { BriefcaseIcon } from '@navikt/aksel-icons';
import { Avatar } from '.';
-import { Badge, DropdownMenu } from '../';
+import { Badge, Dropdown } from '../';
type Story = StoryFn;
@@ -93,33 +93,34 @@ export const WithImage: Story = () => (
);
-export const InDropdownMenu: Story = () => (
-
-
+export const InDropdown: Story = () => (
+
+
ON
Velg Profil
-
-
-
-
+
+
+ Alle kontoer
+
+
ON
Ola Nordmann
-
-
+
+
Sogndal kommune
-
-
-
-
+
+
+
+
);
export const AsLink: Story = () => (
diff --git a/packages/react/src/components/Dropdown/Dropdown.mdx b/packages/react/src/components/Dropdown/Dropdown.mdx
new file mode 100644
index 0000000000..22dedbccc9
--- /dev/null
+++ b/packages/react/src/components/Dropdown/Dropdown.mdx
@@ -0,0 +1,81 @@
+import { Meta, Canvas, Controls, Primary, ArgTypes } from '@storybook/blocks';
+
+import * as DropdownStories from './Dropdown.stories';
+
+import { Dropdown } from './';
+
+
+
+# Dropdown
+
+
+
+
+## Slik bruker du `Dropdown`
+
+```tsx
+import { Dropdown } from '@digdir/designsystemet-react';
+
+// med context
+
+ Trigger
+
+ Heading
+
+ Item
+
+
+
+
+// uten context
+
+
+ Heading
+
+ Item
+
+
+```
+
+## Eksempler på bruk
+
+### Kontrollert
+
+Dersom du sender inn `open`, så bruker du `Dropdown` kontrollert. Du kan bruke `onClose` for å få beskjed når `Dropdown` vil lukkes.
+
+
+
+### Ikoner
+
+Du kan legge ikon rett inn i `Dropdown.Item`, dersom det blir mye mellomrom til kanten kan du legge på din egen klasse og endre på `padding`.
+
+
+
+### Uten Trigger
+
+`Dropdown` bruker popover APIet, så du kan bruke `Dropdown` uten `Dropdown.Trigger`.
+Du må da legge til `popovertarget={id}` på `Dropdown`, og `id` på `Dropdown`.
+
+
+
+## Tilgjengelighet
+
+Det er innebygd tilgjengelighet i `Dropdown.Trigger` med `aria-expanded={true/false}` i henhold til åpne/lukket tilstand, og `aria-haspopup='menu'`.
+
+### `Dropdown.List`
+
+
+
+### `Dropdown.Trigger`
+
+Triggeren er en [Button](/docs/komponenter-button--docs) som standard.
+
+Bruk `Dropdown.Trigger` til å aktivere `Dropdown`. Du kan bruke `asChild` for å endre `Dropdown.Trigger` elementet.
+Dersom du skal legge på funksjoner som `onClick`, legg det på ditt element, og legg `asChild` på `Dropdown.Trigger`.
+
+### Referanser
+
+Vi bruker `ul` og `li` tags i dropdownen, valget er basert på denne:
+
+- https://www.w3.org/WAI/tutorials/menus/flyout/#flyoutnavkbbtn
+- https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/../Dropdown.stories.
diff --git a/packages/react/src/components/Dropdown/Dropdown.stories.tsx b/packages/react/src/components/Dropdown/Dropdown.stories.tsx
new file mode 100644
index 0000000000..2f4e813e96
--- /dev/null
+++ b/packages/react/src/components/Dropdown/Dropdown.stories.tsx
@@ -0,0 +1,117 @@
+import { LinkIcon } from '@navikt/aksel-icons';
+import type { Meta, StoryFn } from '@storybook/react';
+import { useState } from 'react';
+
+import { Dropdown } from '.';
+import { Button } from '../Button';
+
+export default {
+ title: 'Komponenter/Dropdown',
+ component: Dropdown,
+} as Meta;
+
+export const Preview: StoryFn = (args) => {
+ return (
+
+ Dropdown
+
+ Heading 1
+
+ Button 1.1
+ Button 1.2
+
+ Heading 2
+
+ Button 2.1
+ Button 2.2
+
+
+
+ );
+};
+
+Preview.args = {
+ placement: 'bottom-end',
+ size: 'md',
+};
+
+export const Icons: StoryFn = (args) => {
+ return (
+
+ Dropdown
+
+
+
+
+
+ Github
+
+
+
+
+
+ Designsystemet.no
+
+
+
+
+
+ );
+};
+
+export const Controlled: StoryFn = () => {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ setOpen(!open)}>
+ Dropdown
+
+ setOpen(false)}>
+
+
+
+
+ Github
+
+
+
+
+
+ Designsystemet.no
+
+
+
+
+
+ );
+};
+
+export const WithoutTrigger: StoryFn = () => {
+ return (
+ <>
+
+
+
+ Item
+
+
+ >
+ );
+};
diff --git a/packages/react/src/components/Dropdown/Dropdown.test.tsx b/packages/react/src/components/Dropdown/Dropdown.test.tsx
new file mode 100644
index 0000000000..d9c283151c
--- /dev/null
+++ b/packages/react/src/components/Dropdown/Dropdown.test.tsx
@@ -0,0 +1,63 @@
+import { render as renderRtl, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { act } from 'react';
+
+import type { DropdownContextProps } from './DropdownContext';
+
+import { Dropdown } from '.';
+
+const Comp = (args: Partial) => {
+ return (
+
+ Dropdown
+
+ Links
+
+ Item
+ {args.children}
+
+
+
+ );
+};
+
+const render = async (props: Partial = {}) => {
+ /* Flush microtasks */
+ await act(async () => {});
+ const user = userEvent.setup();
+
+ return {
+ user,
+ ...renderRtl(),
+ };
+};
+
+describe('Dropdown', () => {
+ /* We are testing closing and opening in Popover.tests.tsx */
+ it('should render children', async () => {
+ const { user } = await render({
+ children: Item 2,
+ });
+ const dropdownTrigger = screen.getByRole('button');
+
+ await act(async () => await user.click(dropdownTrigger));
+
+ expect(screen.queryByText('Item 2')).toBeInTheDocument();
+ });
+
+ it('should be able to render `Dropdown.Item` as a anchor element using asChild', async () => {
+ const { user } = await render({
+ children: (
+
+ Anchor
+
+ ),
+ });
+ const dropdownTrigger = screen.getByRole('button');
+
+ await act(async () => await user.click(dropdownTrigger));
+
+ expect(screen.getByText('Anchor')).toHaveAttribute('href', '/');
+ expect(screen.getByText('Anchor').tagName).toBe('A');
+ });
+});
diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx
new file mode 100644
index 0000000000..039b7705f7
--- /dev/null
+++ b/packages/react/src/components/Dropdown/Dropdown.tsx
@@ -0,0 +1,54 @@
+import cl from 'clsx/lite';
+import { createContext, forwardRef, useEffect, useState } from 'react';
+import type { ReactNode } from 'react';
+
+import type { Placement } from '@floating-ui/react';
+import { Popover } from '../Popover';
+import type { PopoverProps } from '../Popover';
+
+export type DropdownProps = {
+ /** The placement of the dropdown
+ * @default bottom-end
+ */
+ placement?: Placement;
+ children: ReactNode;
+} & Omit;
+
+export const Dropdown = forwardRef(
+ function DropddownMenuContent(
+ { placement = 'bottom-end', className, ...rest },
+ ref,
+ ) {
+ const [size, setSize] = useState>(
+ rest.size || 'md',
+ );
+
+ useEffect(() => {
+ setSize(rest.size || 'md');
+ }, [rest.size]);
+
+ return (
+
+
+
+ );
+ },
+);
+
+type DropdownMenuCtxType = {
+ size: NonNullable;
+};
+
+export const DropdownCtx = createContext({
+ size: 'md',
+});
diff --git a/packages/react/src/components/Dropdown/DropdownContext.tsx b/packages/react/src/components/Dropdown/DropdownContext.tsx
new file mode 100644
index 0000000000..0ed577de32
--- /dev/null
+++ b/packages/react/src/components/Dropdown/DropdownContext.tsx
@@ -0,0 +1,26 @@
+import type { ReactNode } from 'react';
+
+import { PopoverContext } from '../Popover';
+
+export type DropdownContextProps = {
+ children: ReactNode;
+};
+
+/**
+ * DropdownContext is the root component for the DropdownMenu component.
+ * @example
+ *
+ * Dropdown
+ *
+ * Heading
+ *
+ * Button 1
+ *
+ *
+ *
+ */
+export const DropdownContext = ({ children }: DropdownContextProps) => {
+ return {children};
+};
+
+DropdownContext.displayName = 'DropdownContext';
diff --git a/packages/react/src/components/Dropdown/DropdownHeading.tsx b/packages/react/src/components/Dropdown/DropdownHeading.tsx
new file mode 100644
index 0000000000..666acef743
--- /dev/null
+++ b/packages/react/src/components/Dropdown/DropdownHeading.tsx
@@ -0,0 +1,21 @@
+import cl from 'clsx/lite';
+import { type HTMLAttributes, forwardRef, useContext } from 'react';
+import { Paragraph } from '../Typography';
+import { DropdownCtx } from './Dropdown';
+
+export type DropdownHeadingProps = HTMLAttributes;
+
+export const DropdownHeading = forwardRef<
+ HTMLHeadingElement,
+ DropdownHeadingProps
+>(function DropdownHeading({ children, className, ...rest }, ref) {
+ const { size } = useContext(DropdownCtx);
+
+ return (
+
+
+ {children}
+
+
+ );
+});
diff --git a/packages/react/src/components/Dropdown/DropdownItem.tsx b/packages/react/src/components/Dropdown/DropdownItem.tsx
new file mode 100644
index 0000000000..a3b41d6e60
--- /dev/null
+++ b/packages/react/src/components/Dropdown/DropdownItem.tsx
@@ -0,0 +1,26 @@
+import { forwardRef, useContext } from 'react';
+
+import type { ButtonProps } from '../Button';
+import { Button } from '../Button';
+
+import { DropdownCtx } from './Dropdown';
+
+export type DropdownItemProps = Omit;
+
+export const DropdownItem = forwardRef(
+ function DropdownItem({ className, style, ...rest }, ref) {
+ const { size } = useContext(DropdownCtx);
+
+ return (
+
+
+
+ );
+ },
+);
diff --git a/packages/react/src/components/Dropdown/DropdownList.tsx b/packages/react/src/components/Dropdown/DropdownList.tsx
new file mode 100644
index 0000000000..4fa1b4f1f2
--- /dev/null
+++ b/packages/react/src/components/Dropdown/DropdownList.tsx
@@ -0,0 +1,13 @@
+import cl from 'clsx/lite';
+import { forwardRef } from 'react';
+import type { HTMLAttributes } from 'react';
+
+export type DropdownListProps = HTMLAttributes;
+
+export const DropdownList = forwardRef(
+ function DropdownList({ className, ...rest }, ref) {
+ return (
+
+ );
+ },
+);
diff --git a/packages/react/src/components/Dropdown/DropdownTrigger.tsx b/packages/react/src/components/Dropdown/DropdownTrigger.tsx
new file mode 100644
index 0000000000..bdc42dfc0a
--- /dev/null
+++ b/packages/react/src/components/Dropdown/DropdownTrigger.tsx
@@ -0,0 +1,13 @@
+import { forwardRef } from 'react';
+import type { ComponentPropsWithRef } from 'react';
+
+import { PopoverTrigger } from '../Popover';
+
+export type DropdownTriggerProps = ComponentPropsWithRef;
+
+export const DropdownTrigger = forwardRef<
+ HTMLButtonElement,
+ DropdownTriggerProps
+>(function DropdownTrigger({ asChild, ...rest }, ref) {
+ return ;
+});
diff --git a/packages/react/src/components/Dropdown/index.ts b/packages/react/src/components/Dropdown/index.ts
new file mode 100644
index 0000000000..5f496ab8d8
--- /dev/null
+++ b/packages/react/src/components/Dropdown/index.ts
@@ -0,0 +1,46 @@
+import { Dropdown as DropdownRoot } from './Dropdown';
+import { DropdownContext } from './DropdownContext';
+import { DropdownHeading } from './DropdownHeading';
+import { DropdownItem } from './DropdownItem';
+import { DropdownList } from './DropdownList';
+import { DropdownTrigger } from './DropdownTrigger';
+
+/**
+ * @example
+ *
+ * Dropdown
+ *
+ * Heading
+ *
+ * Button 1
+ *
+ *
+ *
+ */
+const Dropdown = Object.assign(DropdownRoot, {
+ Context: DropdownContext,
+ Heading: DropdownHeading,
+ List: DropdownList,
+ Item: DropdownItem,
+ Trigger: DropdownTrigger,
+});
+
+Dropdown.Context.displayName = 'Dropdown.Context';
+Dropdown.List.displayName = 'Dropdown.List';
+Dropdown.Heading.displayName = 'Dropdown.Heading';
+Dropdown.Item.displayName = 'Dropdown.Item';
+Dropdown.Trigger.displayName = 'Dropdown.Trigger';
+
+export type { DropdownContextProps } from './DropdownContext';
+export type { DropdownListProps } from './DropdownList';
+export type { DropdownHeadingProps } from './DropdownHeading';
+export type { DropdownItemProps } from './DropdownItem';
+export type { DropdownProps } from './Dropdown';
+export {
+ Dropdown,
+ DropdownContext,
+ DropdownList,
+ DropdownHeading,
+ DropdownItem,
+ DropdownTrigger,
+};
diff --git a/packages/react/src/components/DropdownMenu/DropdownMenu.mdx b/packages/react/src/components/DropdownMenu/DropdownMenu.mdx
deleted file mode 100644
index a223cb1572..0000000000
--- a/packages/react/src/components/DropdownMenu/DropdownMenu.mdx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Meta, Canvas, Controls, Primary, ArgTypes } from '@storybook/blocks';
-
-import * as DropdownMenuStories from './DropdownMenu.stories';
-
-import { DropdownMenu } from './';
-
-
-
-# Dropdown
-
-
-
-
-## Slik bruker du `DropdownMenu`
-
-```tsx
-import { DropdownMenu } from '@digdir/designsystemet-react';
-
-
- Trigger
-
-
- Item
-
-
-;
-```
-
-## Eksempler på bruk
-
-### Kontrollert
-
-Dersom du sender inn `open`, så bruker du `DropdownMenu` kontrollert. Du kan bruke `onClose` for å få beskjed når `DropdownMenu` vil lukkes.
-
-
-
-### Ikoner
-
-Du kan legge ikon rett inn i `DropdownMenu.Item`, dersom det blir mye mellomrom til kanten kan du legge på din egen klasse og endre på `padding`.
-
-
-
-### I portal
-
-Legg på `portal` for å rendere `DropdownMenu` i en portal. Dette flytter dropdownen til `body`.
-
-
-
-## Tilgjengelighet
-
-Det er innebygd tilgjengelighet i `DropdownMenu.Trigger` med `aria-expanded={true/false}` i henhold til åpne/lukket tilstand, og `aria-haspopup='menu'`.
-
-### `DropdownMenu.Group`
-
-
-
-### `DropdownMenu.Trigger`
-
-Triggeren er en [Button](/docs/komponenter-button--docs) som standard.
-
-Bruk `DropdownMenu.Trigger` til å aktivere `DropdownMenu`. Du kan bruke `asChild` for å endre `DropdownMenu.Trigger` elementet.
-Dersom du skal legge på funksjoner som `onClick`, legg det på ditt element, og legg `asChild` på `DropdownMenu.Trigger`.
-
-### Referanser
-
-Vi bruker `ul` og `li` tags i dropdownen, valget er basert på denne:
-
-- https://www.w3.org/WAI/tutorials/menus/flyout/#flyoutnavkbbtn
-- https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
diff --git a/packages/react/src/components/DropdownMenu/DropdownMenu.stories.tsx b/packages/react/src/components/DropdownMenu/DropdownMenu.stories.tsx
deleted file mode 100644
index b4cea2f7f2..0000000000
--- a/packages/react/src/components/DropdownMenu/DropdownMenu.stories.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { LinkIcon } from '@navikt/aksel-icons';
-import type { Meta, StoryFn } from '@storybook/react';
-import { useState } from 'react';
-
-import { DropdownMenu } from '.';
-
-const marginDecorator = (Story: StoryFn) => (
-