Skip to content

Commit

Permalink
feat(ScrollView): add interactive prop to support for keyboard cont…
Browse files Browse the repository at this point in the history
…rol (accessible) (#1791)
  • Loading branch information
tujoworker authored Dec 12, 2022
1 parent cb497de commit e265e4a
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
title: 'ScrollView'
description: 'ScrollView is a tiny helper component to allow Eufemia controlling the UX.'
# showTabs: true
description: 'ScrollView is a tiny helper component helping the user controlling overflowing content horizontally or vertically'
showTabs: true
status: null
hideTabs:
- title: Events
- title: Properties
---

import ScrollViewInfo from 'Docs/uilib/components/fragments/scroll-view/info'
import ScrollViewDemos from 'Docs/uilib/components/fragments/scroll-view/demos'

<ScrollViewInfo />
<ScrollViewDemos />
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* UI lib Component Example
*
*/

import React from 'react'
import ComponentBox from 'dnb-design-system-portal/src/shared/tags/ComponentBox'
import { ScrollView } from '@dnb/eufemia/src/fragments'

export const ScrollViewInteractive = () => (
<ComponentBox>
<ScrollView interactive={true} style={{ maxHeight: '10rem' }}>
<div
style={{
minHeight: 800,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
background:
'linear-gradient(rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) 0 0/100% 200%',
}}
>
large content
</div>
</ScrollView>
</ComponentBox>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
showTabs: true
---

import {
ScrollViewInteractive,
} from 'Docs/uilib/components/fragments/scroll-view/Examples'

## Demos

### Keyboard support

When used for regular content, it should be possible for the user to user their keyboard to controll the scrollposition.

You can enable keyboard support with the `interactive` prop.

<ScrollViewInteractive />
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ showTabs: true

# ScrollView

ScrollView is a tiny helper component to allow Eufemia controlling the UX with context to other components.
ScrollView is a tiny helper component helping the user controlling overflowing content horizontally or vertically.

So, it also helps other floating components like a [Dropdown](/uilib/components/dropdown) to ensure that it keeps its floating (Portals) position tied to it's root component.
It also is used in other floating components like [Dropdown](/uilib/components/dropdown) or [Drawer](/uilib/components/drawer).

```jsx
import { ScrollView } from '@dnb/eufemia/fragments'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
showTabs: true
---

## Properties

| Properties | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `interactive` | _(optional)_ set to `true` to make the content accessible to keyboard navigation. Defaults to `false`. |
| [Space](/uilib/components/space/properties) | _(optional)_ spacing properties like `top` or `bottom` are supported. |
10 changes: 5 additions & 5 deletions packages/dnb-eufemia/src/components/drawer/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ToCamelCasePartial } from '../../shared/helpers/withCamelCaseProps'
import { ScrollViewProps } from '../../fragments/ScrollView'
import { ScrollViewAllProps } from '../../fragments/ScrollView'
import { ModalPropTypes } from '../modal/Modal'

export interface DrawerProps extends ToCamelCasePartial<ModalPropTypes> {
export type DrawerProps = {
/**
* Defines the placement on what side the Drawer should be opened. Can be set to `left`, `right`, `top` and `bottom`. Defaults to `right`.
*/
Expand All @@ -12,9 +12,9 @@ export interface DrawerProps extends ToCamelCasePartial<ModalPropTypes> {
* The drawer title. Displays on the very top of the content.
*/
title?: React.ReactNode
}
} & ToCamelCasePartial<ModalPropTypes>

export interface DrawerContentProps extends ScrollViewProps {
export type DrawerContentProps = {
/**
* The minimum Drawer content width, defined by a CSS width value like `50vw` (50% of the viewport). Be careful on using fixed `minWidth` so you don't break responsiveness. Defaults to `30rem` (average width is set to `60vw`).
*/
Expand Down Expand Up @@ -79,4 +79,4 @@ export interface DrawerContentProps extends ScrollViewProps {
* Same as `no_animation`, but gets triggered only if the viewport width is less than `40em`. Defaults to false.
*/
noAnimationOnMobile?: string | boolean
}
} & ScrollViewAllProps
6 changes: 2 additions & 4 deletions packages/dnb-eufemia/src/components/modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { ModalProps } from './types'

import ModalHeader from './parts/ModalHeader'
import ModalHeaderBar from './parts/ModalHeaderBar'
import { ScrollViewProps } from '../../fragments/scroll-view/ScrollView'
import { ScrollViewAllProps } from '../../fragments/scroll-view/ScrollView'
import CloseButton from './parts/CloseButton'
import ModalRoot from './ModalRoot'
import { SpacingProps } from '../../shared/types'
Expand All @@ -42,9 +42,7 @@ interface ModalState {
modalActive: boolean
}

export type ModalPropTypes = ModalProps &
SpacingProps &
Omit<ScrollViewProps, 'title'>
export type ModalPropTypes = ModalProps & SpacingProps & ScrollViewAllProps

class Modal extends React.PureComponent<
ModalPropTypes & ToCamelCasePartial<ModalPropTypes>,
Expand Down
36 changes: 25 additions & 11 deletions packages/dnb-eufemia/src/fragments/scroll-view/ScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,42 @@ import {
import Context from '../../shared/Context'
import { createSpacingClasses } from '../../components/space/SpacingHelper'
import { SpacingProps } from '../../shared/types'
import { includeValidProps } from '../../components/form-row/FormRowHelpers'

export type ScrollViewProps = {
className?: string
children?: React.ReactNode
style?: React.CSSProperties
innerRef?: React.ForwardedRef<unknown>
} & SpacingProps &
Partial<Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>>
/**
* Set to `true` to make the content accessible to keyboard navigation
* Default: false
*/
interactive?: boolean
}

export type ScrollViewAllProps = ScrollViewProps &
SpacingProps &
Partial<Omit<React.HTMLAttributes<HTMLDivElement>, 'title'>> & {
innerRef?: React.ForwardedRef<unknown>
}

const defaultProps = {
children: null,
}

function ScrollView(localProps: ScrollViewProps) {
function ScrollView(localProps: ScrollViewAllProps) {
const context = React.useContext(Context)

// use only the props from context, who are available here anyway
const props = extendPropsWithContext(
localProps,
defaultProps,
includeValidProps(context.FormRow),
context.ScrollView
)

const { children, className = null, innerRef, ...attributes } = props
const {
interactive,
children,
className = null,
innerRef,
...attributes
} = props

const mainParams: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
Expand All @@ -55,11 +65,15 @@ function ScrollView(localProps: ScrollViewProps) {
mainParams.ref = innerRef as React.RefObject<HTMLDivElement>
}

if (interactive) {
mainParams.tabIndex = 0 // Ensure that scrollable region has keyboard access
}

validateDOMAttributes(props, mainParams)

return <div {...mainParams}>{children}</div>
}

export default React.forwardRef((props: ScrollViewProps, ref) => {
export default React.forwardRef((props: ScrollViewAllProps, ref) => {
return <ScrollView {...props} innerRef={ref} />
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'
import { render } from '@testing-library/react'
import ScrollView from '../ScrollView'

describe('ScrollView', () => {
it('should contain children content', () => {
render(<ScrollView>overflow content</ScrollView>)

const element = document.querySelector('.dnb-scroll-view')

expect(element.textContent).toBe('overflow content')
})

it('should set tabindex when interactive is set', () => {
render(<ScrollView interactive>overflow content</ScrollView>)

const element = document.querySelector('.dnb-scroll-view')
const attributes = Array.from(element.attributes).map(
(attr) => attr.name
)

expect(attributes).toEqual(['class', 'tabindex'])
})

it('should include custom classes', () => {
render(
<ScrollView className="custom-class">overflow content</ScrollView>
)

const element = document.querySelector('.dnb-scroll-view')
expect(Array.from(element.classList)).toEqual([
'dnb-scroll-view',
'custom-class',
])
})

it('should support spacing', () => {
render(<ScrollView top="large">overflow content</ScrollView>)

const element = document.querySelector('.dnb-scroll-view')
expect(Array.from(element.classList)).toEqual([
'dnb-scroll-view',
'dnb-space__top--large',
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@
.dnb-scroll-view {
overflow-x: auto;
@include scrollY(auto);

// interactive prop
&[tabindex='0'] {
&:focus {
outline: none;
@include fakeFocus();
}
}
}

0 comments on commit e265e4a

Please sign in to comment.