From f9e9702166f3461dbb6fbe0a02cf3d6694e51c81 Mon Sep 17 00:00:00 2001 From: Artur Paikin Date: Thu, 30 Mar 2023 15:04:55 +0200 Subject: [PATCH] UI: Use form attribite with a form in doc root to prevent outer form submit (#4283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use form attribite with a form in doc root to prevent outer form submit * Fix Unsplash a11y bug — don't focus on hidden links * Combine search and filter into one SearchFilterInput component for Unsplash * Refactor Browser and ProviderView * Refactor FileCard to hooks * Finish SearchFilterInput, add reset labels and no results copy, better styles * don't use debounce for now * combine useEffects, named export, extract RenderMetaFields component * inputCSSClassName --> inputClassName * Remove typo * 🤦 * Patch Preact to work with Jest * tabs vs spaces --------- Co-authored-by: Antoine du Hamel --- .../preact-npm-10.10.0-dd04de05e8.patch | 50 ++- packages/@uppy/core/src/locale.js | 2 + .../components/FileCard/RenderMetaFields.jsx | 46 +++ .../src/components/FileCard/index.jsx | 303 ++++++++---------- packages/@uppy/provider-views/package.json | 1 + packages/@uppy/provider-views/src/Browser.jsx | 64 ++-- packages/@uppy/provider-views/src/Filter.jsx | 51 --- .../src/Item/components/GridLi.jsx | 22 +- .../@uppy/provider-views/src/Item/index.jsx | 1 + .../src/ProviderView/ProviderView.jsx | 26 +- .../provider-views/src/SearchFilterInput.jsx | 101 ++++++ .../src/SearchProviderView/Header.jsx | 32 -- .../src/SearchProviderView/InputView.jsx | 36 --- .../SearchProviderView/SearchProviderView.jsx | 85 ++--- packages/@uppy/provider-views/src/style.scss | 106 +++--- .../uppy-ProviderBrowser-viewType--grid.scss | 44 ++- .../src/style/uppy-SearchProvider-input.scss | 4 + packages/@uppy/unsplash/src/Unsplash.jsx | 1 + packages/@uppy/url/package.json | 1 + packages/@uppy/url/src/UrlUI.jsx | 26 +- yarn.lock | 6 +- 21 files changed, 533 insertions(+), 475 deletions(-) create mode 100644 packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx delete mode 100644 packages/@uppy/provider-views/src/Filter.jsx create mode 100644 packages/@uppy/provider-views/src/SearchFilterInput.jsx delete mode 100644 packages/@uppy/provider-views/src/SearchProviderView/Header.jsx delete mode 100644 packages/@uppy/provider-views/src/SearchProviderView/InputView.jsx diff --git a/.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch b/.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch index e4029d65d8..08aba7da0f 100644 --- a/.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch +++ b/.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch @@ -1,5 +1,53 @@ +diff --git a/debug/package.json b/debug/package.json +index 054944f5478a0a5cf7b6b8791950c595f956157b..06a4fe2719605eb42c5ee795101c21cfd10b59ce 100644 +--- a/debug/package.json ++++ b/debug/package.json +@@ -9,6 +9,7 @@ + "umd:main": "dist/debug.umd.js", + "source": "src/index.js", + "license": "MIT", ++ "type": "module", + "mangle": { + "regex": "^(?!_renderer)^_" + }, +diff --git a/devtools/package.json b/devtools/package.json +index 09b04a77690bdfba01083939ff9eaf987dd50bcb..92c159fbb3cf312c6674202085fb237d6fb921ad 100644 +--- a/devtools/package.json ++++ b/devtools/package.json +@@ -10,6 +10,7 @@ + "source": "src/index.js", + "license": "MIT", + "types": "src/index.d.ts", ++ "type": "module", + "peerDependencies": { + "preact": "^10.0.0" + }, +diff --git a/hooks/package.json b/hooks/package.json +index 74807025bf3de273ebada2cd355428a2c972503d..98501726ffbfe55ffa09928e56a9dcafb9a348ff 100644 +--- a/hooks/package.json ++++ b/hooks/package.json +@@ -10,6 +10,7 @@ + "source": "src/index.js", + "license": "MIT", + "types": "src/index.d.ts", ++ "type": "module", + "scripts": { + "build": "microbundle build --raw", + "dev": "microbundle watch --raw --format cjs", +diff --git a/jsx-runtime/package.json b/jsx-runtime/package.json +index 7a4027831223f16519a74e3028c34f2f8f5f011a..6b58d17dbacce81894467ef43c0a8e2435e388c4 100644 +--- a/jsx-runtime/package.json ++++ b/jsx-runtime/package.json +@@ -10,6 +10,7 @@ + "source": "src/index.js", + "types": "src/index.d.ts", + "license": "MIT", ++ "type": "module", + "peerDependencies": { + "preact": "^10.0.0" + }, diff --git a/package.json b/package.json -index 60279c24a08b808ffbf7dc64a038272bddb6785d..71cb8aa038daeeb7edf43564ed78a219003a0c99 100644 +index 60279c24a08b808ffbf7dc64a038272bddb6785d..088f35fb2c92f2e9b7248557857af2839988d1aa 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ diff --git a/packages/@uppy/core/src/locale.js b/packages/@uppy/core/src/locale.js index 90bcaa9f48..25783f26f9 100644 --- a/packages/@uppy/core/src/locale.js +++ b/packages/@uppy/core/src/locale.js @@ -30,6 +30,7 @@ export default { connectedToInternet: 'Connected to the Internet', // Strings for remote providers noFilesFound: 'You have no files or folders here', + noSearchResults: 'Unfortunately, there are no results for this search', selectX: { 0: 'Select %{smart_count}', 1: 'Select %{smart_count}', @@ -48,6 +49,7 @@ export default { searchImages: 'Search for images', enterTextToSearch: 'Enter text to search for images', search: 'Search', + resetSearch: 'Reset search', emptyFolderAdded: 'No files were added from empty folder', folderAlreadyAdded: 'The folder "%{folder}" was already added', folderAdded: { diff --git a/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx b/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx new file mode 100644 index 0000000000..97984e5d8a --- /dev/null +++ b/packages/@uppy/dashboard/src/components/FileCard/RenderMetaFields.jsx @@ -0,0 +1,46 @@ +import { h } from 'preact' + +export default function RenderMetaFields (props) { + const { + computedMetaFields, + requiredMetaFields, + updateMeta, + form, + formState, + } = props + + const fieldCSSClasses = { + text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input', + } + + return computedMetaFields.map((field) => { + const id = `uppy-Dashboard-FileCard-input-${field.id}` + const required = requiredMetaFields.includes(field.id) + return ( +
+ + {field.render !== undefined + ? field.render({ + value: formState[field.id], + onChange: (newVal) => updateMeta(newVal, field.id), + fieldCSSClasses, + required, + form: form.id, + }, h) + : ( + updateMeta(ev.target.value, field.id)} + data-uppy-super-focusable + /> + )} +
+ ) + }) +} diff --git a/packages/@uppy/dashboard/src/components/FileCard/index.jsx b/packages/@uppy/dashboard/src/components/FileCard/index.jsx index d1a8d4e4a1..c4284f3841 100644 --- a/packages/@uppy/dashboard/src/components/FileCard/index.jsx +++ b/packages/@uppy/dashboard/src/components/FileCard/index.jsx @@ -1,202 +1,153 @@ -import { h, Component } from 'preact' +import { h } from 'preact' +import { useEffect, useState, useCallback } from 'preact/hooks' import classNames from 'classnames' -import { nanoid } from 'nanoid/non-secure' +import { nanoid } from 'nanoid/non-secure' import getFileTypeIcon from '../../utils/getFileTypeIcon.jsx' import ignoreEvent from '../../utils/ignoreEvent.js' import FilePreview from '../FilePreview.jsx' - -class FileCard extends Component { - form = document.createElement('form') - - constructor (props) { - super(props) - - const file = this.props.files[this.props.fileCardFor] - const metaFields = this.getMetaFields() || [] - - const storedMetaData = {} - metaFields.forEach((field) => { - storedMetaData[field.id] = file.meta[field.id] || '' - }) - - this.state = { - formState: storedMetaData, - } - - this.form.id = nanoid() +import RenderMetaFields from './RenderMetaFields.jsx' + +export default function FileCard (props) { + const { + uppy, + files, + fileCardFor, + toggleFileCard, + saveFileCard, + metaFields, + requiredMetaFields, + openFileEditor, + i18n, + i18nArray, + className, + canEditFile, + } = props + + const getMetaFields = () => { + return typeof metaFields === 'function' + ? metaFields(files[fileCardFor]) + : metaFields } - // TODO(aduh95): move this to `UNSAFE_componentWillMount` when updating to Preact X+. - componentWillMount () { // eslint-disable-line react/no-deprecated - this.form.addEventListener('submit', this.handleSave) - document.body.appendChild(this.form) - } - - componentWillUnmount () { - this.form.removeEventListener('submit', this.handleSave) - document.body.removeChild(this.form) - } + const file = files[fileCardFor] + const computedMetaFields = getMetaFields() ?? [] + const showEditButton = canEditFile(file) - getMetaFields () { - return typeof this.props.metaFields === 'function' - ? this.props.metaFields(this.props.files[this.props.fileCardFor]) - : this.props.metaFields - } + const storedMetaData = {} + computedMetaFields.forEach((field) => { + storedMetaData[field.id] = file.meta[field.id] ?? '' + }) - updateMeta = (newVal, name) => { - this.setState(({ formState }) => ({ - formState: { - ...formState, - [name]: newVal, - }, - })) - } + const [formState, setFormState] = useState(storedMetaData) - handleSave = (e) => { - e.preventDefault() - const fileID = this.props.fileCardFor - this.props.saveFileCard(this.state.formState, fileID) - } + const handleSave = useCallback((ev) => { + ev.preventDefault() + saveFileCard(formState, fileCardFor) + }, [saveFileCard, formState, fileCardFor]) - handleCancel = () => { - const file = this.props.files[this.props.fileCardFor] - this.props.uppy.emit('file-editor:cancel', file) - this.props.toggleFileCard(false) + const updateMeta = (newVal, name) => { + setFormState({ [name]: newVal }) } - saveOnEnter = (ev) => { - if (ev.keyCode === 13) { - ev.stopPropagation() - ev.preventDefault() - const file = this.props.files[this.props.fileCardFor] - this.props.saveFileCard(this.state.formState, file.id) - } + const handleCancel = () => { + uppy.emit('file-editor:cancel', file) + toggleFileCard(false) } - renderMetaFields = () => { - const metaFields = this.getMetaFields() || [] - const fieldCSSClasses = { - text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input', + const [form] = useState(() => { + const formEl = document.createElement('form') + formEl.setAttribute('tabindex', '-1') + formEl.id = nanoid() + return formEl + }) + + useEffect(() => { + document.body.appendChild(form) + form.addEventListener('submit', handleSave) + return () => { + form.removeEventListener('submit', handleSave) + document.body.removeChild(form) } + }, [form, handleSave]) + + return ( +
+
+
+ {i18nArray('editing', { + file: {file.meta ? file.meta.name : file.name}, + })} +
+ +
- return metaFields.map((field) => { - const id = `uppy-Dashboard-FileCard-input-${field.id}` - const required = this.props.requiredMetaFields.includes(field.id) - return ( -
- - {field.render !== undefined - ? field.render({ - value: this.state.formState[field.id], - onChange: (newVal) => this.updateMeta(newVal, field.id), - fieldCSSClasses, - required, - form: this.form.id, - }, h) - : ( - . - onKeyUp={'form' in HTMLInputElement.prototype ? undefined : this.saveOnEnter} - onKeyDown={'form' in HTMLInputElement.prototype ? undefined : this.saveOnEnter} - onKeyPress={'form' in HTMLInputElement.prototype ? undefined : this.saveOnEnter} - onInput={ev => this.updateMeta(ev.target.value, field.id)} - data-uppy-super-focusable - /> +
+
+ + {showEditButton + && ( + )} -
- ) - }) - } +
- render () { - const file = this.props.files[this.props.fileCardFor] - const showEditButton = this.props.canEditFile(file) +
+ +
- return ( -
-
-
- {this.props.i18nArray('editing', { - file: {file.meta ? file.meta.name : file.name}, - })} -
+
+
- -
-
- - {showEditButton - && ( - - )} -
- -
- {this.renderMetaFields()} -
- -
- - -
-
- ) - } +
+ ) } - -export default FileCard diff --git a/packages/@uppy/provider-views/package.json b/packages/@uppy/provider-views/package.json index ec4f4f3b0f..92ec8111b3 100644 --- a/packages/@uppy/provider-views/package.json +++ b/packages/@uppy/provider-views/package.json @@ -22,6 +22,7 @@ "dependencies": { "@uppy/utils": "workspace:^", "classnames": "^2.2.6", + "nanoid": "^4.0.0", "preact": "^10.5.13" }, "peerDependencies": { diff --git a/packages/@uppy/provider-views/src/Browser.jsx b/packages/@uppy/provider-views/src/Browser.jsx index 6c0e1b97e3..06d2bf7bd0 100644 --- a/packages/@uppy/provider-views/src/Browser.jsx +++ b/packages/@uppy/provider-views/src/Browser.jsx @@ -1,10 +1,8 @@ import { h } from 'preact' import classNames from 'classnames' - import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal' - -import Filter from './Filter.jsx' +import SearchFilterInput from './SearchFilterInput.jsx' import FooterActions from './FooterActions.jsx' import Item from './Item/index.jsx' @@ -26,13 +24,19 @@ function Browser (props) { showTitles, i18n, validateRestrictions, - showFilter, - filterQuery, - filterInput, + isLoading, + showSearchFilter, + search, + searchTerm, + clearSearch, + searchOnInput, + searchInputLabel, + clearSearchLabel, getNextFolder, cancel, done, columns, + noResultsLabel, } = props const selected = currentSelection.length @@ -44,30 +48,46 @@ function Browser (props) { `uppy-ProviderBrowser-viewType--${viewType}`, )} > -
-
- {headerComponent} + {headerComponent && ( +
+
+ {headerComponent} +
-
+ )} - {showFilter && ( - + {showSearchFilter && ( +
+ +
)} {(() => { + if (isLoading) { + return ( +
+ {i18n('loading')} +
+ ) + } + if (!folders.length && !files.length) { return (
- {i18n('noFilesFound')} + {noResultsLabel}
) } diff --git a/packages/@uppy/provider-views/src/Filter.jsx b/packages/@uppy/provider-views/src/Filter.jsx deleted file mode 100644 index b6ae191e98..0000000000 --- a/packages/@uppy/provider-views/src/Filter.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import { h, Component } from 'preact' - -export default class Filter extends Component { - constructor (props) { - super(props) - this.preventEnterPress = this.preventEnterPress.bind(this) - } - - // eslint-disable-next-line class-methods-use-this - preventEnterPress (ev) { - if (ev.keyCode === 13) { - ev.stopPropagation() - ev.preventDefault() - } - } - - render () { - const { i18n, filterInput, filterQuery } = this.props - return ( -
- filterQuery(e)} - value={filterInput} - /> - - {filterInput && ( - - )} -
- ) - } -} diff --git a/packages/@uppy/provider-views/src/Item/components/GridLi.jsx b/packages/@uppy/provider-views/src/Item/components/GridLi.jsx index 482352cec4..9bebc55e34 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridLi.jsx +++ b/packages/@uppy/provider-views/src/Item/components/GridLi.jsx @@ -1,4 +1,5 @@ import { h } from 'preact' +import classNames from 'classnames' function GridListItem (props) { const { @@ -15,6 +16,13 @@ function GridListItem (props) { children, } = props + const checkBoxClassName = classNames( + 'uppy-u-reset', + 'uppy-ProviderBrowserItem-checkbox', + 'uppy-ProviderBrowserItem-checkbox--grid', + { 'uppy-ProviderBrowserItem-checkbox--is-checked': isChecked }, + ) + return (
  • - - {itemIconEl} - - {showTitles && title} - - {children} - + {itemIconEl} + {showTitles && title} + {children}
  • ) diff --git a/packages/@uppy/provider-views/src/Item/index.jsx b/packages/@uppy/provider-views/src/Item/index.jsx index f3c05cc213..4648bafe68 100644 --- a/packages/@uppy/provider-views/src/Item/index.jsx +++ b/packages/@uppy/provider-views/src/Item/index.jsx @@ -42,6 +42,7 @@ export default (props) => { target="_blank" rel="noopener noreferrer" className="uppy-ProviderBrowserItem-author" + tabIndex="-1" > {author.name} diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx index e7af61f246..8d97cd0931 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx @@ -53,6 +53,7 @@ export default class ProviderView extends View { // Logic this.filterQuery = this.filterQuery.bind(this) + this.clearFilter = this.clearFilter.bind(this) this.getFolder = this.getFolder.bind(this) this.getNextFolder = this.getNextFolder.bind(this) this.logout = this.logout.bind(this) @@ -162,9 +163,12 @@ export default class ProviderView extends View { }).catch(this.handleError) } - filterQuery (e) { - const state = this.plugin.getPluginState() - this.plugin.setPluginState({ ...state, filterInput: e ? e.target.value : '' }) + filterQuery (input) { + this.plugin.setPluginState({ filterInput: input }) + } + + clearFilter () { + this.plugin.setPluginState({ filterInput: '' }) } /** @@ -329,6 +333,7 @@ export default class ProviderView extends View { render (state, viewOptions = {}) { const { authenticated, didFirstRender } = this.plugin.getPluginState() + const { i18n } = this.plugin.uppy if (!didFirstRender) { this.preFirstRender() @@ -346,7 +351,7 @@ export default class ProviderView extends View { title: this.plugin.title, logout: this.logout, username: this.username, - i18n: this.plugin.uppy.i18n, + i18n, } const browserProps = { @@ -360,7 +365,17 @@ export default class ProviderView extends View { getNextFolder: this.getNextFolder, getFolder: this.getFolder, filterItems: this.sharedHandler.filterItems, - filterQuery: this.filterQuery, + + // For SearchFilterInput component + showSearchFilter: targetViewOptions.showFilter, + search: this.filterQuery, + clearSearch: this.clearFilter, + searchTerm: filterInput, + searchOnInput: true, + searchInputLabel: i18n('filter'), + clearSearchLabel: i18n('resetFilter'), + + noResultsLabel: i18n('noFilesFound'), logout: this.logout, handleScroll: this.handleScroll, listAllFiles: this.listAllFiles, @@ -370,7 +385,6 @@ export default class ProviderView extends View { title: this.plugin.title, viewType: targetViewOptions.viewType, showTitles: targetViewOptions.showTitles, - showFilter: targetViewOptions.showFilter, showBreadcrumbs: targetViewOptions.showBreadcrumbs, pluginIcon: this.plugin.icon, i18n: this.plugin.uppy.i18n, diff --git a/packages/@uppy/provider-views/src/SearchFilterInput.jsx b/packages/@uppy/provider-views/src/SearchFilterInput.jsx new file mode 100644 index 0000000000..5177660d58 --- /dev/null +++ b/packages/@uppy/provider-views/src/SearchFilterInput.jsx @@ -0,0 +1,101 @@ +import { h, Fragment } from 'preact' +import { useEffect, useState, useCallback } from 'preact/hooks' +import { nanoid } from 'nanoid/non-secure' +// import debounce from 'lodash.debounce' + +export default function SearchFilterInput (props) { + const { + search, + searchOnInput, + searchTerm, + showButton, + inputLabel, + clearSearchLabel, + buttonLabel, + clearSearch, + inputClassName, + buttonCSSClassName, + } = props + const [searchText, setSearchText] = useState(searchTerm ?? '') + // const debouncedSearch = debounce((q) => search(q), 1000) + + const validateAndSearch = useCallback((ev) => { + ev.preventDefault() + search(searchText) + }, [search, searchText]) + + const handleInput = useCallback((ev) => { + const inputValue = ev.target.value + setSearchText(inputValue) + if (searchOnInput) search(inputValue) + }, [setSearchText, searchOnInput, search]) + + const handleReset = () => { + setSearchText('') + if (clearSearch) clearSearch() + } + + const [form] = useState(() => { + const formEl = document.createElement('form') + formEl.setAttribute('tabindex', '-1') + formEl.id = nanoid() + return formEl + }) + + useEffect(() => { + document.body.appendChild(form) + form.addEventListener('submit', validateAndSearch) + return () => { + form.removeEventListener('submit', validateAndSearch) + document.body.removeChild(form) + } + }, [form, validateAndSearch]) + + return ( + + + { + !showButton && ( + + ) + } + { + !showButton && searchText && ( + + ) + } + { + showButton && ( + + ) + } + + ) +} diff --git a/packages/@uppy/provider-views/src/SearchProviderView/Header.jsx b/packages/@uppy/provider-views/src/SearchProviderView/Header.jsx deleted file mode 100644 index 0a84d9a60f..0000000000 --- a/packages/@uppy/provider-views/src/SearchProviderView/Header.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { h } from 'preact' - -const SUBMIT_KEY = 13 - -export default (props) => { - const { searchTerm, i18n, search } = props - - const handleKeyPress = (ev) => { - if (ev.keyCode === SUBMIT_KEY) { - ev.stopPropagation() - ev.preventDefault() - search(ev.target.value) - } - } - - return ( - - ) -} diff --git a/packages/@uppy/provider-views/src/SearchProviderView/InputView.jsx b/packages/@uppy/provider-views/src/SearchProviderView/InputView.jsx deleted file mode 100644 index 92ea0672b3..0000000000 --- a/packages/@uppy/provider-views/src/SearchProviderView/InputView.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { h } from 'preact' - -export default ({ i18n, search }) => { - let input - const validateAndSearch = () => { - if (input.value) { - search(input.value) - } - } - const handleKeyPress = (ev) => { - if (ev.keyCode === 13) { - validateAndSearch() - } - } - - return ( -
    - { input = input_ }} - data-uppy-super-focusable - /> - -
    - ) -} diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx index cf8b1d8eb9..ad07679f65 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx @@ -1,16 +1,15 @@ import { h } from 'preact' -import SearchInput from './InputView.jsx' +import SearchFilterInput from '../SearchFilterInput.jsx' import Browser from '../Browser.jsx' -import LoaderView from '../Loader.jsx' -import Header from './Header.jsx' import CloseWrapper from '../CloseWrapper.js' import View from '../View.js' import packageJson from '../../package.json' /** - * Class to easily generate generic views for Provider plugins + * SearchProviderView, used for Unsplash and future image search providers. + * Extends generic View, shared with regular providers like Google Drive and Instagram. */ export default class SearchProviderView extends View { static VERSION = packageJson.version @@ -35,7 +34,8 @@ export default class SearchProviderView extends View { // Logic this.search = this.search.bind(this) - this.triggerSearchInput = this.triggerSearchInput.bind(this) + this.clearSearch = this.clearSearch.bind(this) + this.resetPluginState = this.resetPluginState.bind(this) this.addFile = this.addFile.bind(this) this.handleScroll = this.handleScroll.bind(this) this.donePicking = this.donePicking.bind(this) @@ -43,8 +43,7 @@ export default class SearchProviderView extends View { // Visual this.render = this.render.bind(this) - // Set default state for the plugin - this.plugin.setPluginState({ + this.defaultState = { isInputMode: true, files: [], folders: [], @@ -52,7 +51,10 @@ export default class SearchProviderView extends View { filterInput: '', currentSelection: [], searchTerm: null, - }) + } + + // Set default state for the plugin + this.plugin.setPluginState(this.defaultState) } // eslint-disable-next-line class-methods-use-this @@ -60,19 +62,15 @@ export default class SearchProviderView extends View { // Nothing. } - clearSelection () { - this.plugin.setPluginState({ - currentSelection: [], - isInputMode: true, - files: [], - searchTerm: null, - }) + resetPluginState () { + this.plugin.setPluginState(this.defaultState) } #updateFilesAndInputMode (res, files) { this.nextPageQuery = res.nextPageQuery res.items.forEach((item) => { files.push(item) }) this.plugin.setPluginState({ + currentSelection: [], isInputMode: false, files, searchTerm: res.searchedFor, @@ -95,8 +93,12 @@ export default class SearchProviderView extends View { ) } - triggerSearchInput () { - this.plugin.setPluginState({ isInputMode: true }) + clearSearch () { + this.plugin.setPluginState({ + currentSelection: [], + files: [], + searchTerm: null, + }) } async handleScroll (event) { @@ -123,12 +125,13 @@ export default class SearchProviderView extends View { const promises = currentSelection.map((file) => this.addFile(file)) this.sharedHandler.loaderWrapper(Promise.all(promises), () => { - this.clearSelection() + this.resetPluginState() }, () => {}) } render (state, viewOptions = {}) { const { didFirstRender, isInputMode, searchTerm } = this.plugin.getPluginState() + const { i18n } = this.plugin.uppy if (!didFirstRender) { this.preFirstRender() @@ -148,43 +151,49 @@ export default class SearchProviderView extends View { handleScroll: this.handleScroll, done: this.donePicking, cancel: this.cancelPicking, - headerComponent: Header({ - search: this.search, - i18n: this.plugin.uppy.i18n, - searchTerm, - }), + + // For SearchFilterInput component + showSearchFilter: targetViewOptions.showFilter, + search: this.search, + clearSearch: this.clearSearch, + searchTerm, + searchOnInput: false, + searchInputLabel: i18n('search'), + clearSearchLabel: i18n('resetSearch'), + + noResultsLabel: i18n('noSearchResults'), title: this.plugin.title, viewType: targetViewOptions.viewType, showTitles: targetViewOptions.showTitles, showFilter: targetViewOptions.showFilter, + isLoading: loading, showBreadcrumbs: targetViewOptions.showBreadcrumbs, pluginIcon: this.plugin.icon, - i18n: this.plugin.uppy.i18n, + i18n, uppyFiles: this.plugin.uppy.getFiles(), validateRestrictions: (...args) => this.plugin.uppy.validateRestrictions(...args), } - if (loading) { - return ( - - - - ) - } - if (isInputMode) { return ( - - + +
    + +
    ) } return ( - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} diff --git a/packages/@uppy/provider-views/src/style.scss b/packages/@uppy/provider-views/src/style.scss index 66cbfcf884..4ec4a06cd3 100644 --- a/packages/@uppy/provider-views/src/style.scss +++ b/packages/@uppy/provider-views/src/style.scss @@ -203,79 +203,21 @@ vertical-align: middle; } -// Filter - -.uppy-ProviderBrowser-filter { - position: relative; - display: flex; - align-items: center; - width: 100%; - height: 30px; - margin-top: 10px; - margin-bottom: 5px; - background-color: $white; - - [data-uppy-theme="dark"] & { - background-color: $gray-900; - } -} - -.uppy-ProviderBrowser-filterIcon { - position: absolute; - z-index: $zIndex-3; - width: 12px; - height: 12px; - color: $gray-400; - inset-inline-start: 16px; -} - -.uppy-ProviderBrowser-filterInput { - z-index: $zIndex-2; - width: 100%; - height: 30px; - margin: 0 8px; - font-size: 12px; - font-family: $font-family-base; - line-height: 1.4; - background-color: transparent; - border: 0; - border-radius: 4px; - outline: 0; - padding-inline-start: 27px; - - [data-uppy-theme="dark"] & { - color: $gray-200; - background-color: $gray-900; - } -} - -.uppy-ProviderBrowser-filterInput:focus { - background-color: $gray-100; - outline: 0; - - [data-uppy-theme="dark"] & { - background-color: $gray-800; - } -} - -.uppy-ProviderBrowser-filterInput::placeholder { - color: $gray-500; - opacity: 1; -} - // Search -.uppy-ProviderBrowser-search { +.uppy-ProviderBrowser-searchFilter { position: relative; display: flex; align-items: center; width: 100%; height: 30px; - margin-top: 2px; - margin-bottom: 2px; + padding-left: 8px; + padding-right: 8px; + margin-top: 15px; + margin-bottom: 15px; } -.uppy-ProviderBrowser-searchInput { +.uppy-ProviderBrowser-searchFilterInput { z-index: $zIndex-2; width: 100%; height: 30px; @@ -287,37 +229,63 @@ border-radius: 4px; outline: 0; padding-inline-start: 30px; + padding-inline-end: 30px; color: $gray-800; + &::-webkit-search-cancel-button { + display: none; + } + [data-uppy-theme="dark"] & { color: $gray-200; background-color: $gray-900; } } -.uppy-ProviderBrowser-searchInput:focus { +.uppy-ProviderBrowser-searchFilterInput:focus { background-color: $gray-300; - outline: 0; + border: 0; [data-uppy-theme="dark"] & { background-color: $gray-800; } } -.uppy-ProviderBrowser-searchIcon { +.uppy-ProviderBrowser-searchFilterIcon { position: absolute; z-index: $zIndex-3; width: 12px; height: 12px; color: $gray-600; - inset-inline-start: 10px; + inset-inline-start: 16px; } -.uppy-ProviderBrowser-searchInput::placeholder { +.uppy-ProviderBrowser-searchFilterInput::placeholder { color: $gray-500; opacity: 1; } +.uppy-ProviderBrowser-searchFilterReset { + @include blue-border-focus; + border-radius: 3px; + position: absolute; + z-index: $zIndex-3; + width: 22px; + height: 22px; + padding: 6px; + color: $gray-500; + cursor: pointer; + inset-inline-end: 16px; + + &:hover { + color: $gray-600; + } + + svg { + vertical-align: text-top; + } +} + .uppy-ProviderBrowser-userLogout { @include highlight-focus; // for focus diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss index e7ae5170fb..45ca751903 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--grid.scss @@ -81,30 +81,6 @@ text-align: center; border-radius: 4px; - .uppy.uppy-ProviderBrowserItem-inner-relative { - position: relative; - } - - .uppy-ProviderBrowserItem-author { - position: absolute; - display: none; - bottom: 0; - left: 0; - width: 100%; - background: rgba(black, 0.3); - color: white; - font-weight: 500; - font-size: 12px; - margin: 0; - padding: 5px; - text-decoration: none; - - &:hover { - background: rgba(black, 0.4); - text-decoration: underline; - } - } - // Always show the author on touch devices // https://www.w3.org/TR/mediaqueries-4/#hover @media (hover: none) { @@ -125,6 +101,26 @@ } } + .uppy-ProviderBrowserItem-author { + position: absolute; + display: none; + bottom: 0; + left: 0; + width: 100%; + background: rgba(black, 0.3); + color: white; + font-weight: 500; + font-size: 12px; + margin: 0; + padding: 5px; + text-decoration: none; + + &:hover { + background: rgba(black, 0.4); + text-decoration: underline; + } + } + // Checkbox .uppy-ProviderBrowserItem-checkbox { position: absolute; diff --git a/packages/@uppy/provider-views/src/style/uppy-SearchProvider-input.scss b/packages/@uppy/provider-views/src/style/uppy-SearchProvider-input.scss index a7e0ea4d77..10c588b879 100644 --- a/packages/@uppy/provider-views/src/style/uppy-SearchProvider-input.scss +++ b/packages/@uppy/provider-views/src/style/uppy-SearchProvider-input.scss @@ -21,6 +21,10 @@ .uppy-size--md & { margin-bottom: 20px; } + + &::-webkit-search-cancel-button { + display: none; + } } .uppy-SearchProvider-searchButton { diff --git a/packages/@uppy/unsplash/src/Unsplash.jsx b/packages/@uppy/unsplash/src/Unsplash.jsx index fdf79f8bcc..2dafe0990a 100644 --- a/packages/@uppy/unsplash/src/Unsplash.jsx +++ b/packages/@uppy/unsplash/src/Unsplash.jsx @@ -47,6 +47,7 @@ export default class Unsplash extends UIPlugin { this.view = new SearchProviderViews(this, { provider: this.provider, viewType: 'unsplash', + showFilter: true, }) const { target } = this.opts diff --git a/packages/@uppy/url/package.json b/packages/@uppy/url/package.json index 6df1a1e333..32f6cb14c0 100644 --- a/packages/@uppy/url/package.json +++ b/packages/@uppy/url/package.json @@ -25,6 +25,7 @@ "dependencies": { "@uppy/companion-client": "workspace:^", "@uppy/utils": "workspace:^", + "nanoid": "^4.0.0", "preact": "^10.5.13" }, "peerDependencies": { diff --git a/packages/@uppy/url/src/UrlUI.jsx b/packages/@uppy/url/src/UrlUI.jsx index 039b10dac9..f1edf16bb2 100644 --- a/packages/@uppy/url/src/UrlUI.jsx +++ b/packages/@uppy/url/src/UrlUI.jsx @@ -1,17 +1,27 @@ import { h, Component } from 'preact' +import { nanoid } from 'nanoid/non-secure' class UrlUI extends Component { + form = document.createElement('form') + + constructor (props) { + super(props) + this.form.id = nanoid() + } + componentDidMount () { this.input.value = '' + this.form.addEventListener('submit', this.#handleSubmit) + document.body.appendChild(this.form) } - #handleKeyPress = (ev) => { - if (ev.keyCode === 13) { - this.#handleSubmit() - } + componentWillUnmount () { + this.form.removeEventListener('submit', this.#handleSubmit) + document.body.removeChild(this.form) } - #handleSubmit = () => { + #handleSubmit = (ev) => { + ev.preventDefault() const { addFile } = this.props const preparedValue = this.input.value.trim() addFile(preparedValue) @@ -26,14 +36,14 @@ class UrlUI extends Component { type="text" aria-label={i18n('enterUrlToImport')} placeholder={i18n('enterUrlToImport')} - onKeyUp={this.#handleKeyPress} ref={(input) => { this.input = input }} data-uppy-super-focusable + form={this.form.id} /> diff --git a/yarn.lock b/yarn.lock index f2c6a8fe3b..f376926b7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8954,6 +8954,7 @@ __metadata: dependencies: "@uppy/utils": "workspace:^" classnames: ^2.2.6 + nanoid: ^4.0.0 preact: ^10.5.13 peerDependencies: "@uppy/core": "workspace:^" @@ -9162,6 +9163,7 @@ __metadata: dependencies: "@uppy/companion-client": "workspace:^" "@uppy/utils": "workspace:^" + nanoid: ^4.0.0 preact: ^10.5.13 peerDependencies: "@uppy/core": "workspace:^" @@ -27599,8 +27601,8 @@ __metadata: "preact@patch:preact@npm:10.10.0#.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch::locator=%40uppy-dev%2Fbuild%40workspace%3A.": version: 10.10.0 - resolution: "preact@patch:preact@npm%3A10.10.0#.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch::version=10.10.0&hash=a66388&locator=%40uppy-dev%2Fbuild%40workspace%3A." - checksum: f610d7f206e8cd71739023c3dbeae13fcaf9f9e6488295e2ae28f71c615d216c9b1d8b2fa2d616a629276948ee58a8d16fe77b8b7bc2e8d4aee1101e3336fe2b + resolution: "preact@patch:preact@npm%3A10.10.0#.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch::version=10.10.0&hash=e9860f&locator=%40uppy-dev%2Fbuild%40workspace%3A." + checksum: 8fefec89ac68b5bd8fc0c4f1672beb7585c878663f9e929de85ff0f5c6d12dc49d9910867d7c9ef2bc449ec09512241574e08cb0546bdb3a34600891db5e0a96 languageName: node linkType: hard