diff --git a/pkg/webui/components/button-v2/button.styl b/pkg/webui/components/button-v2/button.styl index 91a0612d45..79a71358ed 100644 --- a/pkg/webui/components/button-v2/button.styl +++ b/pkg/webui/components/button-v2/button.styl @@ -14,6 +14,7 @@ .button reset-button() + position: relative display: inline-flex transition: background $ad.s transition-timing-function: ease-out @@ -87,3 +88,6 @@ .icon margin-left: - $cs.xxs + +.dropdown + top: 2.3rem diff --git a/pkg/webui/components/button-v2/index.js b/pkg/webui/components/button-v2/index.js index 3d7a7459af..19f195a9f1 100644 --- a/pkg/webui/components/button-v2/index.js +++ b/pkg/webui/components/button-v2/index.js @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, { useCallback, forwardRef, useMemo } from 'react' +import React, { useCallback, forwardRef, useMemo, useState } from 'react' import classnames from 'classnames' import { useIntl } from 'react-intl' import Icon from '@ttn-lw/components/icon' +import Dropdown from '@ttn-lw/components/dropdown-v2' import Message from '@ttn-lw/lib/components/message' @@ -32,17 +33,17 @@ const filterDataProps = props => return acc }, {}) -const assembleClassnames = ({ message, primary, naked, icon, withDropdown, className }) => +const assembleClassnames = ({ message, primary, naked, icon, dropdownItems, className }) => classnames(style.button, className, { [style.primary]: primary, [style.naked]: naked, [style.withIcon]: icon !== undefined && message, [style.onlyIcon]: icon !== undefined && !message, - [style.withDropdown]: withDropdown, + [style.withDropdown]: Boolean(dropdownItems), }) const buttonChildren = props => { - const { withDropdown, icon, message, children } = props + const { dropdownItems, icon, message, expanded, children } = props const content = Boolean(children) ? ( children @@ -50,7 +51,12 @@ const buttonChildren = props => { <> {icon ? : null} {message ? : null} - {withDropdown ? : null} + {dropdownItems ? ( + <> + + {expanded ? {dropdownItems} : null} + + ) : null} ) @@ -60,7 +66,7 @@ const buttonChildren = props => { const Button = forwardRef((props, ref) => { const { autoFocus, - withDropdown, + dropdownItems, name, type, value, @@ -70,17 +76,40 @@ const Button = forwardRef((props, ref) => { form, ...rest } = props + const [expanded, setExpanded] = useState(false) const dataProps = useMemo(() => filterDataProps(rest), [rest]) + const handleClickOutside = useCallback( + e => { + if (ref.current && !ref.current.contains(e.target)) { + setExpanded(false) + } + }, + [ref], + ) + + const toggleDropdown = useCallback(() => { + setExpanded(oldExpanded => { + const newState = !oldExpanded + if (newState) document.addEventListener('mousedown', handleClickOutside) + else document.removeEventListener('mousedown', handleClickOutside) + return newState + }) + }, [handleClickOutside]) + const handleClick = useCallback( evt => { + if (dropdownItems) { + toggleDropdown() + return + } // Passing a value to the onClick handler is useful for components that // are rendered multiple times, e.g. in a list. The value can be used to // identify the component that was clicked. onClick(evt, value) }, - [onClick, value], + [dropdownItems, onClick, toggleDropdown, value], ) const intl = useIntl() @@ -96,7 +125,7 @@ const Button = forwardRef((props, ref) => { + ) : external ? ( + + {Boolean(iconElement) ? iconElement : null} + + + ) : ( + + {iconElement} + + + ) + return ( +
  • + {ItemElement} +
  • + ) +} + +DropdownItem.propTypes = { + action: PropTypes.func, + active: PropTypes.bool, + exact: PropTypes.bool, + external: PropTypes.bool, + icon: PropTypes.string, + path: PropTypes.string, + showActive: PropTypes.bool, + tabIndex: PropTypes.string, + title: PropTypes.message.isRequired, +} + +DropdownItem.defaultProps = { + active: false, + action: undefined, + exact: false, + external: false, + icon: undefined, + path: undefined, + showActive: true, + tabIndex: '0', +} + +const DropdownHeaderItem = ({ title }) => ( +
  • + + + +
  • +) + +DropdownHeaderItem.propTypes = { + title: PropTypes.message.isRequired, +} + +Dropdown.Item = DropdownItem +Dropdown.HeaderItem = DropdownHeaderItem + +export default Dropdown diff --git a/pkg/webui/components/dropdown-v2/story.js b/pkg/webui/components/dropdown-v2/story.js new file mode 100644 index 0000000000..96d1cb6e32 --- /dev/null +++ b/pkg/webui/components/dropdown-v2/story.js @@ -0,0 +1,32 @@ +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react' + +import Dropdown from '.' + +export default { + title: 'Dropdown V2', + component: Dropdown, +} + +export const Default = () => ( +
    + + + + + +
    +)