mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
ui: an upload target smaller than the dialog around it (#663)
The upload dialog was 682x367 on a 1440x900 desktop -- 47% of the width, 41% of the height -- and the drop target inside it was a strip about 90px tall. A small thing to drag a file onto, with the file browser it came from covering most of the screen behind. Upload and Move ask for `fullScreen`, which is edge to edge on a phone and 90% above the tablet breakpoint: filling a desktop reads as the app having been replaced rather than as something opened over it. Move holds a DirectorySearch above a row per path being moved, which grows with the selection; 620px showed a handful and scrolled the rest. Two things had to follow. Mantine sizes a fullscreen dialog's content to the viewport but leaves the body at its content height, so the footer landed two thirds up the screen with a quarter of the viewport empty beneath it -- the body takes the leftover now and the actions ride to the bottom of it. And the drop target itself grows into the room, with a floor so it stays large once progress bars claim their own space. Rename and the confirmations keep their sizes: a fullscreen dialog for one text field or one sentence reads as broken. New Folder keeps `large` too -- it is a text field and a path preview, the same shape as rename, not the directory picker its name suggests. `size='fullscreen'` is a DIFFERENT thing -- full width, height from the content, which is what a log or a preview wants. Both are documented where the sizes are declared, and the gallery grows a fullscreen example so the difference is visible in all four themes. 339 component tests pass, 69 suites / 1214 jest tests pass, npm run build is clean.
This commit is contained in:
parent
67c3afbec9
commit
82fdf9dd92
5 changed files with 160 additions and 8 deletions
|
|
@ -710,11 +710,11 @@ export function MoveModal({open, paths, onClose, onSubmit}) {
|
|||
</Table.Row>);
|
||||
|
||||
/*
|
||||
* Audited: `large` (620px). This dialog holds a DirectorySearch and a two-column
|
||||
* Source/Destination table of every path being moved, which is the densest content
|
||||
* of the five FileBrowser modals.
|
||||
* Fullscreen: a DirectorySearch above a two-column Source/Destination table of every
|
||||
* path being moved, which grows with the selection. 620px showed a handful of rows and
|
||||
* scrolled the rest.
|
||||
*/
|
||||
return <Modal size='large'
|
||||
return <Modal fullScreen
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
>
|
||||
|
|
@ -925,7 +925,12 @@ export function FileBrowserUploadModal({open, onClose, destination, onComplete})
|
|||
|
||||
const displayPath = destination ? `${mediaDirectory}/${destination}` : mediaDirectory;
|
||||
|
||||
return <Modal open={open} onClose={handleClose} size='large'>
|
||||
/*
|
||||
* Fullscreen, for the drop target. At 620px the target was a strip about 90px tall in a
|
||||
* dialog covering a fifth of the screen -- a small thing to drag a file onto, and the
|
||||
* page behind it was the file browser it came from. Given the room, the target takes it.
|
||||
*/
|
||||
return <Modal open={open} onClose={handleClose} fullScreen>
|
||||
<Modal.Header>Upload to: {displayPath}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<form onSubmit={e => e.preventDefault()}>
|
||||
|
|
@ -940,8 +945,26 @@ export function FileBrowserUploadModal({open, onClose, destination, onComplete})
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<Panel style={{marginTop: '1em', padding: '1em'}}>
|
||||
<div {...getRootProps()} style={{cursor: 'pointer', textAlign: 'center'}}>
|
||||
{/* The target takes the room the dialog gives it rather than sitting as a strip
|
||||
at the top of it. `min-height` as well as `flex`, so it stays a large target
|
||||
once the progress bars below it start claiming their own space. */}
|
||||
<Panel style={{
|
||||
marginTop: '1em',
|
||||
padding: '1em',
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div {...getRootProps()}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: '1 1 auto',
|
||||
minHeight: '30vh',
|
||||
}}>
|
||||
<input {...getInputProps()}/>
|
||||
<Header as='h4' icon='file text'>Click here, or drop files here to upload</Header>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ export function ThemeSamplePage() {
|
|||
const [logLevel, setLogLevel] = useState(3);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [fullModalOpen, setFullModalOpen] = useState(false);
|
||||
const [realColors, setRealColors] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('Videos');
|
||||
|
|
@ -1003,11 +1004,33 @@ export function ThemeSamplePage() {
|
|||
<Panel>
|
||||
<Row>
|
||||
<Button role='cancel' icon='eye' onClick={() => setModalOpen(true)}>Open a modal</Button>
|
||||
<Button role='cancel' icon='expand arrows alternate' onClick={() => setFullModalOpen(true)}>
|
||||
Open a fullscreen modal
|
||||
</Button>
|
||||
</Row>
|
||||
<p style={{fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 0, marginTop: 10}}>
|
||||
A modal is compound — Modal.Header, Modal.Content, Modal.Actions — so the
|
||||
markup at a call site reads the way the dialog is laid out.
|
||||
</p>
|
||||
<p style={{fontSize: '0.75rem', color: 'var(--muted)', marginBottom: 0, marginTop: 10}}>
|
||||
<code>fullScreen</code> is for a dialog whose content wants the room — a drop
|
||||
target, a directory browser, a log. It is edge to edge on a phone and 90% on a
|
||||
desktop, where filling the screen would read as the app having been replaced
|
||||
rather than as something opened over it. Note that the <code>fullscreen</code>
|
||||
<em>size</em> is a different thing: full width, height still from the content.
|
||||
</p>
|
||||
<Modal open={fullModalOpen} onClose={() => setFullModalOpen(false)} fullScreen>
|
||||
<Modal.Header>Upload to: /media/wrolpi</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p style={{marginTop: 0}}>
|
||||
The body takes the leftover height, so the actions sit at the foot of
|
||||
the dialog rather than wherever the content happened to end.
|
||||
</p>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button role='cancel' onClick={() => setFullModalOpen(false)}>Close</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
<Modal open={modalOpen} onClose={() => setModalOpen(false)} size='small'>
|
||||
<Modal.Header>Restore Backup: channels.yaml</Modal.Header>
|
||||
<Modal.Content>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,13 @@ ModalContent.displayName = 'Modal.Content';
|
|||
const ModalActions = ({children}: {children?: React.ReactNode}) => <>{children}</>;
|
||||
ModalActions.displayName = 'Modal.Actions';
|
||||
|
||||
// WROLPi's size names, mapped onto Mantine's scale. See `sizeAliases` in Button.tsx.
|
||||
/*
|
||||
* WROLPi's size names, mapped onto Mantine's scale. See `sizeAliases` in Button.tsx.
|
||||
*
|
||||
* `fullscreen` is the full WIDTH; the height still follows the content, which is what a log
|
||||
* or a preview wants. For a dialog that should fill the viewport in both directions, pass
|
||||
* Mantine's `fullScreen` prop instead -- it forwards through, and the two are not the same.
|
||||
*/
|
||||
const modalSizes: Record<string, string> = {
|
||||
mini: 'xs',
|
||||
tiny: 'sm',
|
||||
|
|
|
|||
|
|
@ -253,3 +253,61 @@ describe('a parent that disables Escape while it is busy', () => {
|
|||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('a dialog that fills the screen', () => {
|
||||
/*
|
||||
* The upload dialog held a drop target about 90px tall inside a dialog covering a fifth
|
||||
* of a desktop. It asks for `fullScreen` now, which is edge to edge on a phone and 90%
|
||||
* on a desktop -- big enough to aim a file at, with the page still framing it.
|
||||
*
|
||||
* Worth a browser test on two counts: the wrapper has to FORWARD `fullScreen` (it
|
||||
* destructures several props and could swallow this one), and the 90% is a media query,
|
||||
* so neither half is visible to jsdom.
|
||||
*/
|
||||
const dialog = <Modal open fullScreen onClose={() => {}}>
|
||||
<Modal.Header>Upload</Modal.Header>
|
||||
<Modal.Content><p>drop files here</p></Modal.Content>
|
||||
<Modal.Actions><Button role='cancel'>Close</Button></Modal.Actions>
|
||||
</Modal>;
|
||||
|
||||
const box = ($el) => $el[0].getBoundingClientRect();
|
||||
|
||||
it('fills a phone edge to edge', () => {
|
||||
cy.viewport(390, 844);
|
||||
cy.mountUI(dialog);
|
||||
|
||||
cy.get('.mantine-Modal-content').should(($content) => {
|
||||
const r = box($content);
|
||||
expect(r.width, 'full width').to.be.closeTo(390, 1);
|
||||
expect(r.height, 'full height').to.be.closeTo(844, 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('stops at 90% on a desktop, so the page still frames it', () => {
|
||||
cy.viewport(1440, 900);
|
||||
cy.mountUI(dialog);
|
||||
|
||||
cy.get('.mantine-Modal-content').should(($content) => {
|
||||
const r = box($content);
|
||||
expect(r.width, '90% of the width').to.be.closeTo(1440 * 0.9, 2);
|
||||
expect(r.height, '90% of the height').to.be.closeTo(900 * 0.9, 2);
|
||||
// Framed rather than merely smaller: page shows on both sides.
|
||||
expect(r.left, 'centred').to.be.greaterThan(0);
|
||||
expect(r.right, 'centred').to.be.lessThan(1440);
|
||||
});
|
||||
});
|
||||
|
||||
it('puts the actions at the bottom of the dialog, not under the content', () => {
|
||||
// Mantine sizes the content to the viewport but leaves the body at its content
|
||||
// height, which left the footer two thirds up the screen with the rest empty.
|
||||
cy.viewport(1440, 900);
|
||||
cy.mountUI(dialog);
|
||||
|
||||
cy.get('.mantine-Modal-content').should(($content) => {
|
||||
const actions = $content[0].querySelector('.wrolpi-modal-actions');
|
||||
expect(actions, 'the dialog has an actions row').to.not.equal(null);
|
||||
expect(box($content).bottom - actions.getBoundingClientRect().bottom,
|
||||
'the actions sit at the foot of the dialog').to.be.at.most(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -494,6 +494,48 @@ html .wrolpi-searchbox-clear {
|
|||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/*
|
||||
* A fullscreen dialog fills its height as well as its width.
|
||||
*
|
||||
* Mantine gives the content 100vh but leaves the body at its content height, so the footer
|
||||
* landed wherever the content ended -- on the upload dialog, two thirds up the screen with a
|
||||
* quarter of the viewport empty beneath it. The body takes the leftover instead, and the
|
||||
* actions ride to the bottom of it.
|
||||
*
|
||||
* `data-full-screen` is Mantine's own marker, so this follows the prop rather than a class a
|
||||
* call site has to remember.
|
||||
*/
|
||||
.mantine-Modal-content[data-full-screen] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*
|
||||
* Edge to edge is for a phone, where the alternative is a dialog with a few pixels of page
|
||||
* showing around it. On a desktop it reads as the app having been replaced rather than as
|
||||
* something opened on top of it, so the dialog stops at 90% and leaves the page framing it.
|
||||
*
|
||||
* 700px is the app's `tablet` breakpoint (`AppMedia` in contexts.js).
|
||||
*/
|
||||
@media (min-width: 700px) {
|
||||
.mantine-Modal-content[data-full-screen] {
|
||||
width: 90vw;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
.mantine-Modal-content[data-full-screen] .mantine-Modal-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mantine-Modal-content[data-full-screen] .wrolpi-modal-actions {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* A confirmation's buttons: under the question, no separating rule. See Confirm. */
|
||||
.wrolpi-confirm-actions {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue