diff --git a/CHANGELOG.md b/CHANGELOG.md index e9bcaf2b6f..5244862c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Expose `Popup`'s content Ref @sophieH29 ([#913](https://github.com/stardust-ui/react/pull/913)) - Fix `Button` Teams theme styles to use semibold weight @notandrew ([#829](https://github.com/stardust-ui/react/pull/829)) +### Documentation +- Add `Editable Area with Dropdown` prototype for mentioning people using `@` character (only available in development mode) @Bugaa92 ([#931](https://github.com/stardust-ui/react/pull/931)) + ## [v0.21.1](https://github.com/stardust-ui/react/tree/v0.21.1) (2019-02-14) [Compare changes](https://github.com/stardust-ui/react/compare/v0.21.0...v0.21.1) diff --git a/docs/src/components/Sidebar/Sidebar.tsx b/docs/src/components/Sidebar/Sidebar.tsx index 1cfdcea662..77d1ae1b39 100644 --- a/docs/src/components/Sidebar/Sidebar.tsx +++ b/docs/src/components/Sidebar/Sidebar.tsx @@ -343,10 +343,10 @@ class Sidebar extends React.Component { styles: menuItemStyles, }, { - key: 'asyncdropdown', - content: 'Async Dropdown Search', + key: 'dropdowns', + content: 'Dropdowns', as: NavLink, - to: '/prototype-async-dropdown-search', + to: '/prototype-dropdowns', styles: menuItemStyles, }, { diff --git a/docs/src/prototypes/AsyncDropdownSearch/index.ts b/docs/src/prototypes/AsyncDropdownSearch/index.ts deleted file mode 100644 index 4f527e450b..0000000000 --- a/docs/src/prototypes/AsyncDropdownSearch/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './AsyncDropdownSearch' diff --git a/docs/src/prototypes/Prototypes.tsx b/docs/src/prototypes/Prototypes.tsx new file mode 100644 index 0000000000..0bc01ae9c4 --- /dev/null +++ b/docs/src/prototypes/Prototypes.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import { Box, Header, Segment } from '@stardust-ui/react' + +interface PrototypeSectionProps { + title?: React.ReactNode +} + +interface ComponentPrototypeProps extends PrototypeSectionProps { + description?: React.ReactNode +} + +export const PrototypeSection: React.FC = props => ( + + {props.title &&
{props.title}
} + {props.children} +
+) + +export const ComponentPrototype: React.FC = props => ( + + {(props.title || props.description) && ( + + {props.title &&
{props.title}
} + {props.description &&

{props.description}

} +
+ )} + {props.children} +
+) diff --git a/docs/src/prototypes/AsyncDropdownSearch/AsyncDropdownSearch.tsx b/docs/src/prototypes/dropdowns/AsyncDropdownSearch.tsx similarity index 79% rename from docs/src/prototypes/AsyncDropdownSearch/AsyncDropdownSearch.tsx rename to docs/src/prototypes/dropdowns/AsyncDropdownSearch.tsx index d082cdee1d..1dfa61f0dd 100644 --- a/docs/src/prototypes/AsyncDropdownSearch/AsyncDropdownSearch.tsx +++ b/docs/src/prototypes/dropdowns/AsyncDropdownSearch.tsx @@ -1,4 +1,4 @@ -import { Divider, Dropdown, DropdownProps, Header, Loader, Segment } from '@stardust-ui/react' +import { Dropdown, DropdownProps, Flex, Label, Loader } from '@stardust-ui/react' import * as faker from 'faker' import * as _ from 'lodash' import * as React from 'react' @@ -54,12 +54,13 @@ class AsyncDropdownSearch extends React.Component<{}, SearchPageState> { fetchItems = () => { clearTimeout(this.searchTimer) - this.setState({ loading: true }) + if (this.state.items.length > 10) return + this.setState({ loading: true }) this.searchTimer = setTimeout(() => { this.setState(prevState => ({ loading: false, - items: [...prevState.items, ..._.times(10, createEntry)], + items: [...prevState.items, ..._.times(2, createEntry)], })) }, 2000) } @@ -68,13 +69,8 @@ class AsyncDropdownSearch extends React.Component<{}, SearchPageState> { const { items, loading, searchQuery, value } = this.state return ( -
- -
-

Use the field to perform a simulated search.

- - - + + { searchQuery={searchQuery} toggleIndicator={false} value={value} + noResultsMessage="We couldn't find any matches" /> - - - -
+ + +
+ + +
+
+ ) } } diff --git a/docs/src/prototypes/dropdowns/MentionsWithDropdown.tsx b/docs/src/prototypes/dropdowns/MentionsWithDropdown.tsx new file mode 100644 index 0000000000..7b84696af0 --- /dev/null +++ b/docs/src/prototypes/dropdowns/MentionsWithDropdown.tsx @@ -0,0 +1,111 @@ +import * as React from 'react' +import * as _ from 'lodash' +import keyboardKey from 'keyboard-key' +import { Dropdown, DropdownProps } from '@stardust-ui/react' + +import { atMentionItems } from './dataMocks' +import { insertTextAtCursorPosition } from './utils' +import { PortalAtCursorPosition } from './PortalAtCursorPosition' + +interface MentionsWithDropdownState { + dropdownOpen?: boolean + searchQuery?: string +} + +const editorStyle: React.CSSProperties = { + backgroundColor: '#eee', + borderRadius: '5px', + border: '1px dashed grey', + padding: '5px', + minHeight: '100px', + outline: 0, +} + +class MentionsWithDropdown extends React.Component<{}, MentionsWithDropdownState> { + private readonly initialState: MentionsWithDropdownState = { + dropdownOpen: false, + searchQuery: '', + } + + private contendEditableRef = React.createRef() + + state = this.initialState + + render() { + const { dropdownOpen, searchQuery } = this.state + + return ( + <> +
+ + + + + ) + } + + private handleEditorKeyUp = (e: React.KeyboardEvent) => { + if (!this.state.dropdownOpen && e.shiftKey && keyboardKey.getCode(e) === keyboardKey.AtSign) { + this.setState({ dropdownOpen: true }) + } + } + + private handleOpenChange = (e: React.SyntheticEvent, { open }: DropdownProps) => { + if (!open) { + this.resetStateAndUpdateEditor() + } + } + + private handleSearchQueryChange = (e: React.SyntheticEvent, { searchQuery }: DropdownProps) => { + this.setState({ searchQuery }) + } + + private handleInputKeyDown = (e: React.KeyboardEvent) => { + const keyCode = keyboardKey.getCode(e) + switch (keyCode) { + case keyboardKey.Backspace: // 8 + if (this.state.searchQuery === '') { + this.resetStateAndUpdateEditor() + } + break + case keyboardKey.Escape: // 27 + this.resetStateAndUpdateEditor() + break + } + } + + private resetStateAndUpdateEditor = () => { + const { searchQuery, dropdownOpen } = this.state + + if (dropdownOpen) { + this.setState(this.initialState, () => { + this.tryFocusEditor() + + // after the dropdown is closed the value of the search query is inserted in the editor at cursor position + insertTextAtCursorPosition(searchQuery) + }) + } + } + + private tryFocusEditor = () => _.invoke(this.contendEditableRef.current, 'focus') +} + +export default MentionsWithDropdown diff --git a/docs/src/prototypes/dropdowns/PortalAtCursorPosition.ts b/docs/src/prototypes/dropdowns/PortalAtCursorPosition.ts new file mode 100644 index 0000000000..6469e2b5d7 --- /dev/null +++ b/docs/src/prototypes/dropdowns/PortalAtCursorPosition.ts @@ -0,0 +1,46 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { insertSpanAtCursorPosition, removeElement } from './utils' + +export interface PortalAtCursorPositionProps { + mountNodeId: string + open?: boolean +} + +export class PortalAtCursorPosition extends React.Component { + private mountNodeInstance: HTMLElement = null + + static defaultProps = { + mountNodeId: 'portal-at-cursor-position', + } + + public componentWillUnmount() { + this.removeMountNode() + } + + public render() { + const { children, open } = this.props + + this.setupMountNode() + return open && this.mountNodeInstance + ? ReactDOM.createPortal(children, this.mountNodeInstance) + : null + } + + private setupMountNode = () => { + const { mountNodeId, open } = this.props + + if (open) { + this.mountNodeInstance = this.mountNodeInstance || insertSpanAtCursorPosition(mountNodeId) + } else { + this.removeMountNode() + } + } + + private removeMountNode = () => { + if (this.mountNodeInstance) { + removeElement(this.mountNodeInstance) + this.mountNodeInstance = null + } + } +} diff --git a/docs/src/prototypes/dropdowns/dataMocks.ts b/docs/src/prototypes/dropdowns/dataMocks.ts new file mode 100644 index 0000000000..3ebcc80b6a --- /dev/null +++ b/docs/src/prototypes/dropdowns/dataMocks.ts @@ -0,0 +1,14 @@ +import * as _ from 'lodash' +import { name, internet } from 'faker' + +interface AtMentionItem { + header: string + image: string + content: string +} + +export const atMentionItems: AtMentionItem[] = _.times(10, () => ({ + header: `${name.firstName()} ${name.lastName()}`, + image: internet.avatar(), + content: name.title(), +})) diff --git a/docs/src/prototypes/dropdowns/index.tsx b/docs/src/prototypes/dropdowns/index.tsx new file mode 100644 index 0000000000..83666c8e43 --- /dev/null +++ b/docs/src/prototypes/dropdowns/index.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' +import { PrototypeSection, ComponentPrototype } from '../Prototypes' +import AsyncDropdownSearch from './AsyncDropdownSearch' +import MentionsWithDropdown from './MentionsWithDropdown' + +export default () => ( + + + + + + + + +) diff --git a/docs/src/prototypes/dropdowns/utils.ts b/docs/src/prototypes/dropdowns/utils.ts new file mode 100644 index 0000000000..03ffc6fa52 --- /dev/null +++ b/docs/src/prototypes/dropdowns/utils.ts @@ -0,0 +1,51 @@ +const getRangeAtCursorPosition = () => { + if (!window.getSelection) { + return null + } + + const sel = window.getSelection() + if (!sel.getRangeAt || !sel.rangeCount) { + return null + } + + return sel.getRangeAt(0) +} + +export const insertSpanAtCursorPosition = (id: string) => { + if (!id) { + throw '[insertSpanAtCursorPosition]: id must be supplied' + } + + const range = getRangeAtCursorPosition() + if (!range) { + return null + } + + const elem = document.createElement('span') + elem.id = id + range.insertNode(elem) + + return elem +} + +export const insertTextAtCursorPosition = (text: string) => { + if (!text) { + return null + } + + const range = getRangeAtCursorPosition() + if (!range) { + return null + } + + const textNode = document.createTextNode(text) + range.insertNode(textNode) + range.setStartAfter(textNode) + + return textNode +} + +export const removeElement = (element: string | HTMLElement): HTMLElement => { + const elementToRemove = typeof element === 'string' ? document.getElementById(element) : element + return elementToRemove.parentNode.removeChild(elementToRemove) +} diff --git a/docs/src/routes.tsx b/docs/src/routes.tsx index b9a1490e9f..ee59aaf4c5 100644 --- a/docs/src/routes.tsx +++ b/docs/src/routes.tsx @@ -62,9 +62,9 @@ const Router = () => ( />, , string diff --git a/packages/react/src/components/Dropdown/DropdownSearchInput.tsx b/packages/react/src/components/Dropdown/DropdownSearchInput.tsx index 56c5ceb6fc..f7fee4351d 100644 --- a/packages/react/src/components/Dropdown/DropdownSearchInput.tsx +++ b/packages/react/src/components/Dropdown/DropdownSearchInput.tsx @@ -111,9 +111,11 @@ class DropdownSearchInput extends UIComponent ) } diff --git a/packages/react/src/components/Input/Input.tsx b/packages/react/src/components/Input/Input.tsx index 8f6819e24a..0c195754a0 100644 --- a/packages/react/src/components/Input/Input.tsx +++ b/packages/react/src/components/Input/Input.tsx @@ -124,7 +124,6 @@ class Input extends AutoControlledComponent, InputState> renderComponent({ accessibility, ElementType, - classes, unhandledProps, styles, variables, diff --git a/packages/react/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts b/packages/react/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts index 7452a15b6b..237ebe0b07 100644 --- a/packages/react/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts +++ b/packages/react/src/themes/teams/components/Dropdown/dropdownSearchInputStyles.ts @@ -16,8 +16,8 @@ const dropdownSearchInputStyles: ComponentSlotStylesInput< backgroundColor: 'transparent', borderWidth: 0, ...(p.inline && { - paddingLeft: 0, - paddingRight: 0, + padding: 0, + lineHeight: 'initial', }), }), } diff --git a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts index d6a7c808b4..468d41015b 100644 --- a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts +++ b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts @@ -74,7 +74,10 @@ const dropdownStyles: ComponentSlotStylesInput ({