Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Features
- Add focus styles for `Menu.Item` component @Bugaa92 ([#286](https://github.com/stardust-ui/react/pull/286))
- Add keyboard handling and ARIA attributes for `ButtonGroup`, `Tablist` and `Toolbar` behaviors @jurokapsiar ([#254](https://github.com/stardust-ui/react/pull/254))
- Add autocontrolled mode for `Popup` @kuzhelov ([#319](https://github.com/stardust-ui/react/pull/319)
- Improve accessibility behaviors @sophieH29 ([#247](https://github.com/stardust-ui/react/pull/247))

### Documentation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import { Button, Input, Popup } from '@stardust-ui/react'

class PopupControlledExample extends React.Component<any, any> {
state = { popupOpen: false }

togglePopup() {
this.setState(prev => ({ popupOpen: !prev.popupOpen }))
}

render() {
return (
<Popup
open={this.state.popupOpen}
onOpenChange={(e, newProps) => {
alert(`Popup is requested to change its open state to "${newProps.open}".`)
this.setState({ popupOpen: newProps.open })
}}
trigger={<Button icon="expand" onClick={() => this.togglePopup()} />}
content={<Input icon="search" placeholder="Search..." />}
/>
)
}
}

export default PopupControlledExample
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react'
import { Button, Input, Popup } from '@stardust-ui/react'

class PopupControlledExample extends React.Component<any, any> {
state = { popupOpen: false }

togglePopup() {
this.setState(prev => ({ popupOpen: !prev.popupOpen }))
}

render() {
return (
<Popup
open={this.state.popupOpen}
onOpenChange={(e, newProps) => {
alert(`Popup is requested to change its open state to "${newProps.open}".`)
this.setState({ popupOpen: newProps.open })
}}
content={<Input icon="search" placeholder="Search..." />}
>
<Button icon="expand" onClick={() => this.togglePopup()} />
</Popup>
)
}
}

export default PopupControlledExample
Original file line number Diff line number Diff line change
@@ -1,26 +1,6 @@
import React from 'react'
import { Button, Input, Popup } from '@stardust-ui/react'
import { Button, Popup } from '@stardust-ui/react'

class PopupExample extends React.Component<any, any> {
state = { popupOpen: false }

togglePopup() {
this.setState(prev => ({ popupOpen: !prev.popupOpen }))
}

render() {
return (
<Popup
open={this.state.popupOpen}
onOpenChange={(e, newProps) => {
alert(`Popup is requested to change its open state to "${newProps.open}".`)
this.setState({ popupOpen: newProps.open })
}}
trigger={<Button icon="expand" onClick={() => this.togglePopup()} />}
content={<Input icon="search" placeholder="Search..." />}
/>
)
}
}
const PopupExample = () => <Popup trigger={<Button icon="expand" />} content="Hello from popup!" />

export default PopupExample
29 changes: 6 additions & 23 deletions docs/src/examples/components/Popup/Types/PopupExample.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import React from 'react'
import { Button, Input, Popup } from '@stardust-ui/react'
import { Button, Popup } from '@stardust-ui/react'

class PopupExample extends React.Component<any, any> {
state = { popupOpen: false }

togglePopup() {
this.setState(prev => ({ popupOpen: !prev.popupOpen }))
}

render() {
return (
<Popup
open={this.state.popupOpen}
onOpenChange={(e, newProps) => {
alert(`Popup is requested to change its open state to "${newProps.open}".`)
this.setState({ popupOpen: newProps.open })
}}
content={<Input icon="search" placeholder="Search..." />}
>
<Button icon="expand" onClick={() => this.togglePopup()} />
</Popup>
)
}
}
const PopupExample = () => (
<Popup content="Hello from popup!">
<Button icon="expand" />
</Popup>
)

export default PopupExample
7 changes: 6 additions & 1 deletion docs/src/examples/components/Popup/Types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ const Types = () => (
<ExampleSection title="Types">
<ComponentExample
title="Default"
description="A default popup. Note that Popup is a controlled component, and its 'open' prop value could be changed either by parent component, or by user actions (e.g. key press) - thus it is necessary to handle 'onOpenChanged' event. Try to type some text into popup's input field and press ESC to see the effect."
description="A default popup."
examplePath="components/Popup/Types/PopupExample"
/>
<ComponentExample
title="Controlled"
description="Note that if Popup is controlled, then its 'open' prop value could be changed either by parent component, or by user actions (e.g. key press) - thus it is necessary to handle 'onOpenChanged' event. Try to type some text into popup's input field and press ESC to see the effect."
examplePath="components/Popup/Types/PopupControlledExample"
/>
</ExampleSection>
)

Expand Down
31 changes: 21 additions & 10 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface IPopupProps {
}

export interface IPopupState {
open: boolean
triggerRef: HTMLElement
}

Expand Down Expand Up @@ -111,23 +112,22 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
private static isBrowserContext = isBrowser()

protected actionHandlers: AccessibilityActionHandlers = {
toggle: e =>
_.invoke(this.props, 'onOpenChange', e, { ...this.props, ...{ open: !this.props.open } }),
toggle: e => this.trySetOpen(!this.state.open, e, true),
closeAndFocusTrigger: e => {
if (this.props.onOpenChange) {
_.invoke(this.props, 'onOpenChange', e, { ...this.props, ...{ open: false } })
_.invoke(this.state.triggerRef, 'focus')
}
this.trySetOpen(false, e, true)
_.invoke(this.state.triggerRef, 'focus')
},
}

public state = { triggerRef: undefined }
public state = { triggerRef: undefined, open: false }

public renderComponent({
rtl,
accessibility,
}: IRenderResultConfig<IPopupProps>): React.ReactNode {
const { children, trigger, open } = this.props
const { children, trigger } = this.props

const triggerElement = childrenExist(children) ? children : (trigger as any)

return (
<>
Expand All @@ -136,12 +136,17 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
this.setState({ triggerRef: domNode })
}}
>
{React.cloneElement(childrenExist(children) ? children : (trigger as any), {
{React.cloneElement(triggerElement, {
onClick: e => {
this.trySetOpen(!this.state.open, e)
_.invoke(triggerElement, 'props.onClick', e)
},
...accessibility.attributes.trigger,
...accessibility.keyHandlers.trigger,
})}
</Ref>
{open &&

{this.state.open &&
Popup.isBrowserContext &&
createPortal(this.renderPopupContent(rtl, accessibility), document.body)}
</>
Expand Down Expand Up @@ -185,4 +190,10 @@ export default class Popup extends AutoControlledComponent<Extendable<IPopupProp
</Ref>
)
}

private trySetOpen(newValue: boolean, eventArgs: any, forceChangeEvent: boolean = false) {
if (this.trySetState({ open: newValue }) || forceChangeEvent) {
_.invoke(this.props, 'onOpenChange', eventArgs, { ...this.props, ...{ open: newValue } })
}
}
}
9 changes: 7 additions & 2 deletions src/lib/AutoControlledComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export default class AutoControlledComponent<P = {}, S = {}> extends UIComponent
* @param {object} maybeState State that corresponds to controlled props.
* @param {object} [state] Actual state, useful when you also need to setState.
*/
trySetState = (maybeState, state?) => {
trySetState = (maybeState, state?): boolean => {
const { autoControlledProps } = this.constructor as any
if (process.env.NODE_ENV !== 'production') {
const { name } = this.constructor
Expand Down Expand Up @@ -221,6 +221,11 @@ export default class AutoControlledComponent<P = {}, S = {}> extends UIComponent

if (state) newState = { ...newState, ...state }

if (Object.keys(newState).length > 0) this.setState(newState)
if (Object.keys(newState).length > 0) {
this.setState(newState)
return true
}

return false
}
}