diff --git a/src/components/tw-fonts-modal/add-system-font.jsx b/src/components/tw-fonts-modal/add-system-font.jsx index c06fd83784e..0618668ab77 100644 --- a/src/components/tw-fonts-modal/add-system-font.jsx +++ b/src/components/tw-fonts-modal/add-system-font.jsx @@ -64,24 +64,13 @@ class AddSystemFont extends React.Component { />

- {/* TODO: datalist is pretty bad at this. we should try our own dropdown? */} - {this.state.localFonts && ( - - {this.state.localFonts.map(family => ( - - )} {this.state.name && ( diff --git a/src/components/tw-fonts-modal/font-dropdown-item.jsx b/src/components/tw-fonts-modal/font-dropdown-item.jsx new file mode 100644 index 00000000000..316ce390b33 --- /dev/null +++ b/src/components/tw-fonts-modal/font-dropdown-item.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import bindAll from 'lodash.bindall'; +import styles from './fonts-modal.css'; + +class FontDropdownItem extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleSelect' + ]); + } + + handleSelect () { + this.props.onSelect(this.props.family); + } + + render () { + return ( +
+ {this.props.family} +
+ ); + } +} + +FontDropdownItem.propTypes = { + family: PropTypes.string.isRequired, + onSelect: PropTypes.func.isRequired +}; + +export default FontDropdownItem; diff --git a/src/components/tw-fonts-modal/font-name.jsx b/src/components/tw-fonts-modal/font-name.jsx index 22108be0fce..4e02663d35d 100644 --- a/src/components/tw-fonts-modal/font-name.jsx +++ b/src/components/tw-fonts-modal/font-name.jsx @@ -1,53 +1,142 @@ import React from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import styles from './fonts-modal.css'; import bindAll from 'lodash.bindall'; +import styles from './fonts-modal.css'; +import FontDropdownItem from './font-dropdown-item.jsx'; class FontName extends React.Component { constructor (props) { super(props); bindAll(this, [ + 'setInputRef', 'handleChange', - 'handleFlush', - 'handleKeyPress' + 'handleFocus', + 'handleBlur', + 'handleResize', + 'handleSelectFont', + 'handleKeyDown' ]); + this.state = { + focused: false, + rect: null + }; + } + + componentDidMount () { + window.addEventListener('resize', this.handleResize); + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + setInputRef (input) { + this.input = input; + + // can't use autoFocus because handleFocus relies on the ref existing already + if (input) { + input.focus(); + } } handleChange (e) { this.props.onChange(e.target.value); } - handleFlush () { + handleFocus () { + this.setState({ + focused: true, + rect: this.input.getBoundingClientRect() + }); + } + + handleBlur () { this.props.onChange(this.props.fontManager.getSafeName(this.props.name)); + this.setState({ + focused: false + }); } - handleKeyPress (e) { + handleResize () { + if (this.state.focused) { + this.setState({ + rect: this.input.getBoundingClientRect() + }); + } + } + + handleSelectFont (font) { + this.props.onChange(font); + } + + handleKeyDown (e) { if (e.key === 'Enter') { - this.handleFlush(); + this.handleBlur(); e.target.blur(); } } + getFilteredOptions () { + if (!this.state.focused || !this.props.options) { + return []; + } + const name = this.props.name.toLowerCase(); + const candidates = this.props.options + .filter(family => family.toLowerCase().includes(name)); + if (candidates.length === 0 && candidates[0] === this.props.name) { + return []; + } + return candidates; + } + render () { const { /* eslint-disable no-unused-vars */ name, onChange, fontManager, + options, /* eslint-enable no-unused-vars */ ...props } = this.props; + + const filteredOptions = this.getFilteredOptions(); return ( - +
+ + + {/* We need to use a portal to get out of the modal's overflow: hidden, unfortunately */} + {filteredOptions.length > 0 && ReactDOM.createPortal( +
+ {this.getFilteredOptions().map(family => ( + + ))} +
, + document.body + )} +
); } } @@ -57,7 +146,8 @@ FontName.propTypes = { onChange: PropTypes.func.isRequired, fontManager: PropTypes.shape({ getSafeName: PropTypes.func.isRequired - }).isRequired + }).isRequired, + options: PropTypes.arrayOf(PropTypes.string.isRequired) }; export default FontName; diff --git a/src/components/tw-fonts-modal/fonts-modal.css b/src/components/tw-fonts-modal/fonts-modal.css index f54ce978d63..57387e236aa 100644 --- a/src/components/tw-fonts-modal/fonts-modal.css +++ b/src/components/tw-fonts-modal/fonts-modal.css @@ -1,4 +1,5 @@ @import "../../css/colors.css"; +@import "../../css/z-index.css"; .modal-content { max-width: 550px; @@ -66,6 +67,9 @@ } +.font-input-outer { + +} .font-input { width: 100%; border: 1px solid $ui-black-transparent; @@ -75,6 +79,34 @@ font: inherit; } +.font-dropdown-outer { + position: absolute; + z-index: $z-index-modal; + background-color: white; + color: $text-primary; + border-radius: 0.25rem; + overflow: auto; + max-height: 300px; + border: 1px solid $ui-black-transparent; + box-sizing: border-box; + box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3); +} +[theme="dark"] .font-dropdown-outer { + background-color: $ui-secondary; +} +.font-dropdown-item { + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + height: 1.5rem; + cursor: pointer; + transition: .1s ease; +} +.font-dropdown-item:hover { + background-color: $motion-primary; + color: #ffffff; +} + .font-playground { background: none; border: none; diff --git a/src/components/tw-security-manager-modal/data-url.css b/src/components/tw-security-manager-modal/data-url.css new file mode 100644 index 00000000000..d9d58b0eb0c --- /dev/null +++ b/src/components/tw-security-manager-modal/data-url.css @@ -0,0 +1,20 @@ +@import "../../css/colors.css"; + +.code { + display: block; + width: 100%; + max-width: 100%; + min-width: 100%; + height: 5rem; + min-height: 3rem; + border: 1px solid $ui-black-transparent; + border-radius: 0.25rem; + padding: 0.25rem; + font-size: 0.875rem; + font-family: monospace; + margin: 0.5rem 0; +} +[theme="dark"] .code { + background: $ui-secondary; + color: white; +} diff --git a/src/components/tw-security-manager-modal/data-url.jsx b/src/components/tw-security-manager-modal/data-url.jsx new file mode 100644 index 00000000000..94eb423c06c --- /dev/null +++ b/src/components/tw-security-manager-modal/data-url.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styles from './data-url.css'; + +/** + * @param {string} dataURI data: URI + * @returns {string} A hopefully human-readable version + */ +const decodeDataURI = dataURI => { + const delimeter = dataURI.indexOf(','); + if (delimeter === -1) { + return dataURI; + } + const contentType = dataURI.substring(0, delimeter); + const data = dataURI.substring(delimeter + 1); + if (contentType.endsWith(';base64')) { + try { + return atob(data); + } catch (e) { + return dataURI; + } + } + try { + return decodeURIComponent(data); + } catch (e) { + return dataURI; + } +}; + +const DataURL = props => ( +