UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

932 lines (855 loc) 26.2 kB
--- title: 'DrawerList' description: 'The DrawerList component is a fragment inside other components.' version: 11.0.0 generatedAt: 2026-04-21T13:57:51.404Z checksum: 090b7d977ba4be5e2c4c04d199a30a4048416c59f443a56985df2f80629d9c40 --- # DrawerList ## Import ```tsx import { DrawerList } from '@dnb/eufemia/fragments' ``` ## Description The DrawerList component is a fragment inside other components. It is used e.g. in the [Dropdown](/uilib/components/dropdown). ## Relevant links - [Source code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-eufemia/src/components/fragments/drawer-list) - [Docs code](https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-design-system-portal/src/docs/uilib/components/fragments/drawer-list) ## Data structure ```js // as array const data = [ // Every data item can, beside "content" - contain what ever { // (optional) can be what ever selectedKey: 'key_0', // Item content as a string or array content: 'Item 1 Content', }, // more items ... { selectedKey: 'key_1', content: ['Item 2 Value', 'Item 2 Content'], }, { selectedKey: 'key_2', content: ['Item 3 Content A', 'Item 3 Content B'], }, { selectedKey: 'key_3', content: ['Item 4 Content A', <>Custom Component</>], }, ] // as object const data = { a: 'A', b: 'B', } ``` ### Example usage of `optionsRender` ```tsx render( <Wrapper> <ComponentBox scope={{ data, }} hidePreview hideToolbar > <DrawerList optionsRender={({ Items, Item, data }) => ( <> <Items /> <Item>Addition</Item> {data.length > 1 && <li>Addition</li>} </> )} /> </ComponentBox> </Wrapper> ) ``` ### data-dnb-drawer-list-active When a DrawerList is open, it will set an HTML attribute on the main HTML Element called `data-dnb-drawer-list-active`. The attribute value will be the ID of the current DrawerList. This can be used to handle z-index issues from within CSS only: ```css html[data-dnb-drawer-list-active='DrawerList-ID'] { /* Your css */ } ``` ## Demos ### Default DrawerList, triggered by a ToggleButton ```tsx render( <Wrapper> <ComponentBox scope={{ data, }} > {() => { const DrawerListWithState = (props) => { const [open, setOpen] = React.useState(false) return ( <> <ToggleButton id="state-toggle-button" text="Toggle" checked={open} icon={`chevron_${open ? 'up' : 'down'}`} iconPosition="left" onChange={({ checked }) => setOpen(checked)} /> <DrawerList wrapperElement="#state-toggle-button" skipPortal data={data} open={open} onClose={() => setOpen(false)} {...props} /> </> ) } return <DrawerListWithState /> }} </ComponentBox> </Wrapper> ) ``` ### DrawerList list - only to visualize ```tsx render( <Wrapper> <ComponentBox data-visual-test="drawer-list" scope={{ data, }} hideCode > <span className="dnb-drawer-list__list"> <ul className="dnb-drawer-list__options"> <li className="dnb-drawer-list__option first-of-type"> <span className="dnb-drawer-list__option__inner"> Brukskonto - Kari Nordmann </span> </li> <li className="dnb-drawer-list__option dnb-drawer-list__option--selected"> <span className="dnb-drawer-list__option__inner"> <span className="dnb-drawer-list__option__item item-nr-1"> <NumberFormat.BankAccountNumber> 12345678902 </NumberFormat.BankAccountNumber> </span> <span className="dnb-drawer-list__option__item"> Sparekonto - Ole Nordmann </span> </span> </li> <li className="dnb-drawer-list__option"> <span className="dnb-drawer-list__option__inner"> <span className="dnb-drawer-list__option__item item-nr-1"> <NumberFormat.BankAccountNumber> 11345678962 </NumberFormat.BankAccountNumber> </span> <span className="dnb-drawer-list__option__item item-nr-2"> <a className="dnb-anchor dnb-anchor--has-icon" href="/uilib/components/fragments/drawer-list/" > Long link that will wrap over several lines </a> </span> <span className="dnb-drawer-list__option__item"> Feriekonto - Kari Nordmann med et kjempelangt etternavnsen </span> </span> </li> <li className="dnb-drawer-list__option last-of-type"> <span className="dnb-drawer-list__option__inner"> <span className="dnb-drawer-list__option__item item-nr-1"> <NumberFormat.BankAccountNumber> 15349648901 </NumberFormat.BankAccountNumber> </span> <span className="dnb-drawer-list__option__item"> Oppussing - Ole Nordmann </span> </span> </li> <li className="dnb-drawer-list__arrow" /> </ul> </span> </ComponentBox> </Wrapper> ) ``` ### Default DrawerList ```tsx render( <Wrapper> <ComponentBox scope={{ data, }} > <DrawerList skipPortal open preventClose arrowPosition="left" data={data} value={3} onChange={({ data: selectedDataItem }) => { console.log('onChange', selectedDataItem) }} onOpen={() => { console.log('onOpen') }} observerElement=".dnb-live-preview" // prevents direction to change when scrolling in this example /> </ComponentBox> </Wrapper> ) ``` ### Disabled ```tsx render( <Wrapper> <ComponentBox data-visual-test="drawer-list-disabled"> <DrawerList skipPortal open preventClose data={[ { content: 'Item 1', }, { content: 'Item 2, disabled', disabled: true, }, { content: 'Item 3', }, ]} observerElement=".dnb-live-preview" // prevents direction to change when scrolling in this example /> </ComponentBox> </Wrapper> ) ``` ### Custom event and link on single item ```tsx render( <Wrapper> <ComponentBox scope={{ data, }} > {() => { const CustomComponent = () => ( <CustomComponentInner onTouchStart={preventDefault} onClick={(e) => { console.log('Do something different') preventDefault(e) }} > Custom event handler </CustomComponentInner> ) const CustomComponentInner = styled.span` display: block; width: 100%; margin: -1rem -2rem -1rem -1rem; padding: 1rem 2rem 1rem 1rem; ` const preventDefault = (e) => { e.stopPropagation() e.preventDefault() } const CustomWidth = styled(DrawerList)` .dnb-drawer-list__list { width: var(--drawer-list-width); } ` return ( <CustomWidth skipPortal open preventClose right title="Choose an item" data={() => [ <Link key="link" href="/"> Go to this Link </Link>, 'Or press on me', <CustomComponent key="custom" />, ]} onChange={({ value }) => { console.log('More menu:', value) }} suffix={ <HelpButton title="Modal Title">Modal content</HelpButton> } observerElement=".dnb-live-preview" // prevents direction to change when scrolling in this example /> ) }} </ComponentBox> </Wrapper> ) ``` ### Using List and Items markup **NB:** By using this method you lose currently a lot of the core functionality like keyboard support and other accessibility features. ```tsx render( <Wrapper> <ComponentBox> {() => { const list = [ { value: 'A', }, { value: 'B', }, { value: 'C', }, ] const CustomWidth = styled(DrawerList)` .dnb-drawer-list__list { width: var(--drawer-list-width); } ` const DrawerListWithState = () => { const [selected, setSelected] = React.useState('C') return ( <CustomWidth skipPortal open preventClose> <DrawerList.Options> {list.map(({ value, ...props }, i) => ( <DrawerList.Item key={i} selected={value === selected} value={value} onClick={({ value }) => setSelected(value)} {...props} > {value} </DrawerList.Item> ))} </DrawerList.Options> </CustomWidth> ) } return <DrawerListWithState /> }} </ComponentBox> </Wrapper> ) ``` ### Inline styling using JSX ```tsx render( <Wrapper> <ComponentBox> <DrawerList skipPortal open preventClose observerElement=".dnb-live-preview" // prevents direction to change when scrolling in this example > <DrawerList.Options> <DrawerList.Item style={{ color: 'red', }} key="A" selected={false} value="A" onClick={() => { console.log('onClick') }} > Item 1 </DrawerList.Item> <DrawerList.HorizontalItem style={{ color: 'green', }} key="B" selected={false} value="B" > Item 2 </DrawerList.HorizontalItem> </DrawerList.Options> </DrawerList> </ComponentBox> </Wrapper> ) ``` ### Inline styling using `data` ```tsx render( <Wrapper> <ComponentBox data-visual-test="drawer-list-inline-style"> <DrawerList skipPortal open preventClose data={[ { content: 'They may be very large, like pneumonoultramicroscopicsilicovolcanoconiosis, a 45-letter hippopotomonstrosesquipedalian word for black lung disease.', style: { hyphens: 'auto', color: 'red', }, }, { content: 'The longest word in the Oxford English Dictionary is the 45-letter pneumonoultramicroscopicsilicovolcanoconiosis, which refers to a form of lung disease.', style: { hyphens: 'none', color: 'green', }, }, { content: 'According to the Oxford English Dictionary the longest word in the language is pneumonoultramicroscopicsilicovolcanoconiosis, with 45 letters.', style: { hyphens: 'manual', color: 'blue', }, }, ]} observerElement=".dnb-live-preview" // prevents direction to change when scrolling in this example /> </ComponentBox> </Wrapper> ) ``` ### Groups If an item has a `groupIndex` property, it will use the groups in the `groups` property. Only the first group can be without title, all other groups must have a title. ```tsx render( <Wrapper> <ComponentBox data-visual-test="drawer-list-groups"> <DrawerList skipPortal open preventClose observerElement=".dnb-live-preview" // prevents direction to change when scrolling in this example groups={[undefined, 'Pets', undefined, 'Cars']} data={[ { groupIndex: 0, content: 'Default 1', }, { groupIndex: 0, content: 'Default 2', }, { groupIndex: 1, content: 'Cat', }, { groupIndex: 1, content: 'Dog', }, { groupIndex: 2, content: 'Something', }, { groupIndex: 3, content: 'Jeep', }, { groupIndex: 3, content: 'Van', }, { content: 'No group', }, ]} /> </ComponentBox> </Wrapper> ) ``` ## Properties ```json { "props": { "[data](#the-data-property)": { "doc": "The data we want to fill the list with. [Details on the type of {DATA} can be found below](#the-data-property). The data can be provided as an array or object. Or as a function that returns the data (called when user opens the list).", "type": ["{DATA}", "() => {DATA}"], "status": "required" }, "groups": { "doc": "An array of group titles for the list items. Only the first group can be `undefined`.", "type": "Array<React.ReactNode>", "status": "optional" }, "value": { "doc": "Define a preselected `data` entry. In order of priority, `value` can be set to: object key (if `data` is an object), `selectedKey` property (if `data` is an array), array index (if no `selectedKey`) or content (if `value` is a non-integer string).", "type": ["string", "number"], "status": "optional" }, "defaultValue": { "doc": "Define a startup value or handle a re-render without handling the state during the re-render by yourself. Defaults to `null`.", "type": "number", "status": "optional" }, "arrowPosition": { "doc": "Position of the arrow on the popup drawer. Set to `left` or `right`. Defaults to `left` if not set.", "type": "string", "status": "optional" }, "direction": { "doc": "Defines the direction of how the drawer-list shows the options list. Can be `bottom` or `top`. Defaults to `auto`.", "type": ["\"auto\"", "\"top\"", "\"bottom\""], "status": "optional" }, "labelDirection": { "doc": "Use `labelDirection=\"horizontal\"` to change the label layout direction. Defaults to `vertical`.", "type": "string", "status": "optional" }, "preventSelection": { "doc": "If set to `true`, the DrawerList will then not make any permanent selection.", "type": "boolean", "status": "optional" }, "focusable": { "doc": "If set to `true`, the element is then focusable by assertive technologies.", "type": "boolean", "status": "optional" }, "preventClose": { "doc": "If set to `true`, the DrawerList will not close on any events.", "type": "boolean", "status": "optional" }, "keepOpen": { "doc": "If set to `true`, the DrawerList will close on outside clicks, but not on selection.", "type": "boolean", "status": "optional" }, "independentWidth": { "doc": "If set to `true`, the DrawerList will handle its width and position independently of the parent/mother element.", "type": "boolean", "status": "optional" }, "fixedPosition": { "doc": "If set to `true`, the DrawerList will be fixed in its scroll position by using CSS `position: fixed;`.", "type": "boolean", "status": "optional" }, "enableBodyLock": { "doc": "If set to `true`, the HTML body will get locked from scrolling when the Dropdown is open.", "type": "boolean", "status": "optional" }, "skipKeysearch": { "doc": "If set to `true`, search items by the first key will be ignored.", "type": "boolean", "status": "optional" }, "ignoreEvents": { "doc": "If set to `true`, all keyboard and mouse events will be ignored.", "type": "boolean", "status": "optional" }, "alignDrawer": { "doc": "Use 'right' to change the options alignment direction. Makes only sense to use in combination with `preventSelection` - or if an independent width is used.", "type": "string", "status": "optional" }, "listClass": { "doc": "Define an HTML class that will be set on the list, beside `dnb-drawer-list__list`.", "type": "string", "status": "optional" }, "portalClass": { "doc": "Define an HTML class that will be set on the DOM portal beside `dnb-drawer-list__portal__style`. Can be useful to handle e.g. a custom `z-index` in relation to a header.", "type": "string", "status": "optional" }, "scrollable": { "doc": "Defines if the options list should be scrollable (the `max-height` is set by default to `50vh`).", "type": "boolean", "status": "optional" }, "noScrollAnimation": { "doc": "To disable scrolling animation.", "type": "boolean", "status": "optional" }, "noAnimation": { "doc": "To disable appear/disappear (show/hide) animation.", "type": "boolean", "status": "optional" }, "skipPortal": { "doc": "To disable the React Portal behavior.", "type": "boolean", "status": "optional" }, "minHeight": { "doc": "Defines the minimum height (in `rem`) of the options list.", "type": "string", "status": "optional" }, "maxHeight": { "doc": "Defines the maximum height (in `rem`) of the options list.", "type": "string", "status": "optional" }, "pageOffset": { "doc": "Defines the available scrollable height. If scrolling should not change the height of the drawer-list, then set it to `0` (useful if the DrawerList is used in fixed positions on contrast to a scrollable page content).", "type": "string", "status": "optional" }, "observerElement": { "doc": "Set a HTML element, either as a selector or a DOM element. Can be used to send in an element which will be used to make the direction calculation on.", "type": "string", "status": "optional" }, "cacheHash": { "doc": "Set a `cacheHash` as a string to enable internal memorizing of the list to enhance rerendering performance. Components like Autocomplete are using this because of the huge data changes due to search and reorder.", "type": "string", "status": "optional" }, "wrapperElement": { "doc": "Has to be an HTML Element, or a selector for one, ideally a mother element, used to calculate sizes and distances. Also used for the 'click outside' detection. Clicking on the `wrapperElement` will not trigger an outside click.", "type": ["string", "HTMLElement"], "status": "optional" }, "optionsRender": { "doc": "Has to be a function, returning the items again. See [example](/uilib/components/fragments/drawer-list#example-usage-of-optionsRender). This can be used to add additional options above the actual rendered list.", "type": "function", "status": "optional" }, "[Space](/uilib/layout/space/properties)": { "doc": "Spacing properties like `top` or `bottom` are supported.", "type": ["string", "object"], "status": "optional" } } } ``` ## The `data` property The `data` can be structured in two main ways: - As an array - As an object. An array is preferred as it gives you the most options. ### `data` as an array ```ts // an array can contain complex items and offers the most control const data = [ { content: "Item 1", }, { content: <span>Item 2</span> }, { content: ["Item 3", "Line 2", <span>Line 3</span>] }, { content: ['Main account', '1234 12 12345'], selectedValue: 'Main account (605,22 kr)', suffixValue: '605,22 kr', }, { content: ['Old account', <i>Closed</i>], disabled: true, suffixValue: '0,00 kr', }, ] // If you only use the `content` property, you can use it directly in the array. // This list is identical to the one above: const data = [ "Item 1", <span>Item 2</span>, ["Item 3", "Line 2", <span>Line 3</span>], { content: ['Main account', '1234 12 12345'], selectedValue: 'Main account (605,22 kr)', suffixValue: '605,22 kr', }, { content: ['Old account', <i>Closed</i>], disabled: true, suffixValue: '0,00 kr', }, ] const onChange = ({ data, value }) => { console.log(data) // returns the item as it appears in the array console.log(value) // returns the index of the item } ``` Each object in the array have the following properties: ```json { "props": { "content": { "doc": "Visual content in the list item.", "type": [ "string", "React.ReactNode", "Array<(string | React.ReactNode)>" ], "status": "optional" }, "disabled": { "doc": "Disables the list item from selection.", "type": "boolean", "status": "optional" }, "groupIndex": { "doc": "What group index in the `groups` property this item belongs to.", "type": "number", "status": "optional" }, "selectedKey": { "doc": "If set, can be used instead of array index by the `value` prop.", "type": ["string", "number"], "status": "optional" }, "selectedValue": { "doc": "Replaces the standard value output for selected item. Only used in some implementations (Dropdown, Autocomplete).", "type": ["string", "React.ReactNode"], "status": "optional" }, "suffixValue": { "doc": "Content placed to the right in the list item.", "type": ["string", "React.ReactNode"], "status": "optional" } } } ``` ### `data` as an object A simpler alternative, but with less options ```ts // Each entry can contain the same type of value as the array's `content` property const data = { first: "Item 1",, second: <span>Item 2</span>, last: ["Item 3", "Line 2", <span>Line 3</span>], } const onChange = ({ data, value }) => { console.log(data) // returns a generated object representing the item: // { // selectedKey: 'first', // value: 'first', // content: 'Item 1', // type: 'object' // } console.log(value) // returns the key ("first", "second", or "last"), instead of an index } ``` ### `data` types overview The following is an overview of all the types that the `data` property accepts. (These are not actual names of actual types in the library.) ```ts // The visual content that is shown in one DrawerList item. // An array can be used to define multiple lines. type CONTENT = string | React.ReactNode | (string | React.ReactNode)[] // An array item type ARRAY_OBJECT = { content: CONTENT disabled?: boolean selectedKey?: string | number selectedValue?: string | React.ReactNode suffixValue?: string | React.ReactNode style?: React.CSSProperties } // `data` as an array. A list of "ARRAY_OBJECT" types is preferred, // but the "CONTENT" type can be useful for simple lists. type ARRAY = (CONTENT | ARRAY_OBJECT)[] // `data` as an object. Can only contain the "CONTENT" type. // Each `key` behaves like the "ARRAY_OBJECT"'s `selectedKey`. type RECORD = Record<string, CONTENT> // An object or array that represents the entire DrawerList list. type DATA = ARRAY | RECORD // The final type of the `data` property: let data: DATA | () => DATA ``` #### JSON string There is technically support for sending in a JSON string of the data to the `data` property. But this is an old functionality that we do not really support anymore. ## Translations ```json { "locales": ["da-DK", "en-GB", "nb-NO", "sv-SE"], "entries": { "DrawerList.defaultGroupSR": { "nb-NO": "Standardvalg", "en-GB": "Default options", "sv-SE": "Standardval", "da-DK": "Standardvalg" }, "DrawerList.missingGroup": { "nb-NO": "Gruppe", "en-GB": "Group", "sv-SE": "Grupp", "da-DK": "Gruppe" }, "DrawerList.noGroupSR": { "nb-NO": "Andre valg", "en-GB": "Other options", "sv-SE": "Andra val", "da-DK": "Andre valg" } } } ``` ## Events ```json { "props": { "onPreChange": { "doc": "Will be called before `onChange`, this way you can return false to prevent selection and to prevent `onChange` execution.", "type": "function", "status": "optional" }, "onChange": { "doc": "Will be called on state changes made by the user.", "type": "function", "status": "optional" }, "onSelect": { "doc": "Will be called once the user focuses or selects an item by a click or keyboard navigation.", "type": "function", "status": "optional" }, "onOpen": { "doc": "Will be called once the user presses the drawer-list.", "type": "function", "status": "optional" }, "onClose": { "doc": "Will be called once the user presses the drawer-list again, or clicks somewhere else.", "type": "function", "status": "optional" } } } ``` ### The `onChange` vs `onSelect` difference The difference between `onChange` and `onSelect` is: - `onChange` will be called when the state changes, either with a **click** or **space/enter** keypress confirmation. - `onSelect` differs most when the user is navigating by keyboard. Once the user is pressing e.g. the arrow keys, the selection is changing, but not the state.