Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react'
import { Grid, Dropdown } from '@stardust-ui/react'

const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred Pennyworth']

const DropdownExamplePosition = () => (
<Grid columns="1, 80px" variables={{ padding: '30px' }}>
<Dropdown
items={inputItems}
placeholder="Select your hero"
align="start"
position="above"
offset="-100%p"
/>
</Grid>
)

export default DropdownExamplePosition
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react'
import { Grid, Dropdown } from '@stardust-ui/react'

const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred Pennyworth']

const DropdownArrowExample = props => {
const { position, align } = props

return (
<Dropdown
items={inputItems}
placeholder={`${position} the trigger aligned to the ${align}.`}
align={align}
position={position}
/>
)
}

const triggers = [
{ position: 'above', align: 'start' },
{ position: 'above', align: 'end' },
{ position: 'below', align: 'start' },
{ position: 'below', align: 'end' },
{ position: 'before', align: 'top' },
{ position: 'before', align: 'bottom' },
{ position: 'after', align: 'top' },
{ position: 'after', align: 'bottom' },
]

const DropdownExamplePosition = () => (
<Grid columns="repeat(2, 1fr)" variables={{ padding: '100px', gridGap: '100px' }}>
{triggers.map(({ position, align }) => (
<DropdownArrowExample position={position} align={align} key={`${position}-${align}`} />
))}
</Grid>
)

export default DropdownExamplePosition
10 changes: 10 additions & 0 deletions docs/src/examples/components/Dropdown/Variations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ const Variations = () => (
description="A multiple search dropdown that uses French to provide information and accessibility."
examplePath="components/Dropdown/Variations/DropdownExampleSearchMultipleFrenchLanguage"
/>
<ComponentExample
title="Alignment and Position"
description="A dropdown can be positioned around its trigger and aligned relative to the trigger's margins. Click on a button to open a dropdown on a specific position and alignment."
examplePath="components/Dropdown/Variations/DropdownExamplePosition"
/>
<ComponentExample
title="Offset"
description="Dropdown position could be further customized by providing offset value. Note that percentage values of both trigger and dropdown elements' lengths are supported."
examplePath="components/Dropdown/Variations/DropdownExampleOffset"
/>
</ExampleSection>
)

Expand Down
2 changes: 1 addition & 1 deletion packages/react-component-ref/src/Ref.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface RefProps {
*
* @param {HTMLElement} node - Referred node.
*/
innerRef: React.Ref<any>
innerRef: React.Ref<HTMLElement>
}

const Ref: React.FunctionComponent<RefProps> = props => {
Expand Down
70 changes: 56 additions & 14 deletions packages/react/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
commonPropTypes,
UIComponentProps,
} from '../../lib'
import List from '../List/List'
import List, { ListProps } from '../List/List'
import DropdownItem, { DropdownItemProps } from './DropdownItem'
import DropdownSelectedItem, { DropdownSelectedItemProps } from './DropdownSelectedItem'
import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchInput'
Expand All @@ -39,6 +39,13 @@ import { screenReaderContainerStyles } from '../../lib/accessibility/Styles/acce
import ListItem from '../List/ListItem'
import Icon, { IconProps } from '../Icon/Icon'
import Portal from '../Portal/Portal'
import {
ALIGNMENTS,
POSITIONS,
Positioner,
PositionCommonProps,
UpdatableComponent,
} from '../../lib/positioner'

export interface DropdownSlotClassNames {
clearIndicator: string
Expand All @@ -52,7 +59,9 @@ export interface DropdownSlotClassNames {
triggerButton: string
}

export interface DropdownProps extends UIComponentProps<DropdownProps, DropdownState> {
export interface DropdownProps
extends UIComponentProps<DropdownProps, DropdownState>,
PositionCommonProps {
/** The index of the currently active selected item, if dropdown has a multiple selection. */
activeSelectedIndex?: number

Expand Down Expand Up @@ -236,6 +245,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
content: false,
}),
activeSelectedIndex: PropTypes.number,
align: PropTypes.oneOf(_.without(ALIGNMENTS)),
clearable: PropTypes.bool,
clearIndicator: customPropTypes.itemShorthand,
defaultActiveSelectedIndex: PropTypes.number,
Expand Down Expand Up @@ -264,6 +274,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
onSelectedChange: PropTypes.func,
open: PropTypes.bool,
placeholder: PropTypes.string,
position: PropTypes.oneOf(POSITIONS),
renderItem: PropTypes.func,
renderSelectedItem: PropTypes.func,
search: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
Expand All @@ -278,6 +289,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
}

static defaultProps: DropdownProps = {
align: 'start',
as: 'div',
clearIndicator: 'stardust-close',
itemToString: item => {
Expand All @@ -288,6 +300,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
// targets DropdownItem shorthand objects
return (item as any).header || String(item)
},
position: 'below',
toggleIndicator: {},
triggerButton: {},
}
Expand Down Expand Up @@ -421,7 +434,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
},
}),
})}
{this.renderItemsList(
{this.preparePropsAndRenderItemsList(
styles,
variables,
highlightedIndex,
Expand All @@ -430,6 +443,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
getMenuProps,
getItemProps,
getInputProps,
rtl,
)}
</div>
</Ref>
Expand Down Expand Up @@ -518,7 +532,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
})
}

private renderItemsList(
private preparePropsAndRenderItemsList(
styles: ComponentSlotStylesInput,
variables: ComponentVariablesInput,
highlightedIndex: number,
Expand All @@ -527,6 +541,7 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
getMenuProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any,
getItemProps: (options: GetItemPropsOptions<ShorthandValue>) => any,
getInputProps: (options?: GetInputPropsOptions) => any,
rtl: boolean,
) {
const { search } = this.props
const { open } = this.state
Expand Down Expand Up @@ -559,20 +574,47 @@ class Dropdown extends AutoControlledComponent<Extendable<DropdownProps>, Dropdo
handleRef(innerRef, listElement)
}}
>
<List
className={Dropdown.slotClassNames.itemsList}
{...accessibilityMenuProps}
styles={styles.list}
tabIndex={search ? undefined : -1} // needs to be focused when trigger button is activated.
aria-hidden={!open}
onFocus={this.handleTriggerButtonOrListFocus}
onBlur={this.handleListBlur}
items={open ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : []}
/>
{this.renderItemsList(
{
className: Dropdown.slotClassNames.itemsList,
...accessibilityMenuProps,
styles: styles.list,
tabIndex: search ? undefined : -1, // needs to be focused when trigger button is activated.
'aria-hidden': !open,
onFocus: this.handleTriggerButtonOrListFocus,
onBlur: this.handleListBlur,
items: open ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : [],
},
rtl,
)}
</Ref>
)
}

private renderItemsList(listProps: ListProps, rtl: boolean): JSX.Element {
const { align, position, offset } = this.props

return (
<Positioner
align={align}
position={position}
offset={offset}
rtl={rtl}
target={this.selectedItemsRef}
children={popperChildrenProps => (
<UpdatableComponent
Component={List}
innerRef={popperChildrenProps.ref}
scheduleUpdate={popperChildrenProps.scheduleUpdate}
updateDependencies={[listProps.items.length]}
style={popperChildrenProps.style}
{...listProps}
/>
)}
/>
)
}

private renderItems(
styles: ComponentSlotStylesInput,
variables: ComponentVariablesInput,
Expand Down
59 changes: 11 additions & 48 deletions packages/react/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as ReactDOM from 'react-dom'
import * as PropTypes from 'prop-types'
import * as keyboardKey from 'keyboard-key'
import * as _ from 'lodash'
import { Popper, PopperChildrenProps } from 'react-popper'
import { PopperChildrenProps } from 'react-popper'

import {
applyAccessibilityKeyHandlers,
Expand All @@ -24,12 +24,8 @@ import {
setWhatInputSource,
} from '../../lib'
import { ComponentEventHandler, ReactProps, ShorthandValue } from '../../types'

import { getPopupPlacement, applyRtlToOffset, Alignment, Position } from './positioningHelper'
import createPopperReferenceProxy from './createPopperReferenceProxy'

import { ALIGNMENTS, POSITIONS, Positioner, PositionCommonProps } from '../../lib/positioner'
import PopupContent from './PopupContent'

import { popupBehavior } from '../../lib/accessibility'
import {
AutoFocusZone,
Expand All @@ -44,9 +40,6 @@ import {
AccessibilityBehavior,
} from '../../lib/accessibility/types'

const POSITIONS: Position[] = ['above', 'below', 'before', 'after']
const ALIGNMENTS: Alignment[] = ['top', 'bottom', 'start', 'end', 'center']

export type PopupEvents = 'click' | 'hover' | 'focus'
export type RestrictedClickEvents = 'click' | 'focus'
export type RestrictedHoverEvents = 'hover' | 'focus'
Expand All @@ -59,17 +52,15 @@ export interface PopupSlotClassNames {
export interface PopupProps
extends StyledComponentProps<PopupProps>,
ChildrenComponentProps,
ContentComponentProps<ShorthandValue> {
ContentComponentProps<ShorthandValue>,
PositionCommonProps {
/**
* Accessibility behavior if overridden by the user.
* @default popupBehavior
* @available popupFocusTrapBehavior, dialogBehavior
* */
accessibility?: Accessibility

/** Alignment for the popup. */
align?: Alignment

/** Additional CSS class name(s) to apply. */
className?: string

Expand All @@ -88,15 +79,6 @@ export interface PopupProps
/** Delay in ms for the mouse leave event, before the popup will be closed. */
mouseLeaveDelay?: number

/** Offset value to apply to rendered popup. Accepts the following units:
* - px or unit-less, interpreted as pixels
* - %, percentage relative to the length of the trigger element
* - %p, percentage relative to the length of the popup element
* - vw, CSS viewport width unit
* - vh, CSS viewport height unit
*/
offset?: string

/** Events triggering the popup. */
on?: PopupEvents | PopupEventsArray

Expand All @@ -113,13 +95,6 @@ export interface PopupProps
/** A popup can show a pointer to trigger. */
pointing?: boolean

/**
* Position for the popup. Position has higher priority than align. If position is vertical ('above' | 'below')
* and align is also vertical ('top' | 'bottom') or if both position and align are horizontal ('before' | 'after'
* and 'start' | 'end' respectively), then provided value for 'align' will be ignored and 'center' will be used instead.
*/
position?: Position

/**
* Function to render popup content.
* @param {Function} updatePosition - function to request popup position update.
Expand All @@ -140,7 +115,6 @@ export interface PopupProps

export interface PopupState {
open: boolean
target: HTMLElement
}

/**
Expand Down Expand Up @@ -409,27 +383,16 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
rtl: boolean,
accessibility: AccessibilityBehavior,
): JSX.Element {
const { align, position, offset } = this.props
const { target } = this.props

const placement = getPopupPlacement({ align, position, rtl })

const popperModifiers = {
// https://popper.js.org/popper-documentation.html#modifiers..offset
...(offset && {
offset: { offset: rtl ? applyRtlToOffset(offset, position) : offset },
keepTogether: { enabled: false },
}),
}

const referenceElement = createPopperReferenceProxy(target || this.triggerRef)
const { align, position, offset, target } = this.props

return (
<Popper
placement={placement}
referenceElement={referenceElement}
<Positioner
align={align}
position={position}
offset={offset}
rtl={rtl}
target={target || this.triggerRef}
children={this.renderPopperChildren.bind(this, popupPositionClasses, rtl, accessibility)}
modifiers={popperModifiers}
/>
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export {
PopupEventsArray,
} from './components/Popup/Popup'
export { default as PopupContent, PopupContentProps } from './components/Popup/PopupContent'
export { Placement, Alignment, Position } from './components/Popup/positioningHelper'
export { Alignment, Position } from './lib/positioner'

export {
default as Portal,
Expand Down
Loading