Allow multi-panel to keep one open only

This commit is contained in:
Jake Archibald
2018-10-24 11:20:38 +02:00
parent b34dca744d
commit 04492f8f5e
6 changed files with 97 additions and 40 deletions

View File

@@ -1,8 +1,10 @@
import './styles.css'; import * as style from './styles.css';
const openOneOnlyAttr = 'open-one-only';
function getClosestHeading(el: Element) { function getClosestHeading(el: Element) {
const closestEl = el.closest('multi-panel > *'); const closestEl = el.closest('multi-panel > *');
if (closestEl && closestEl.classList.contains('panel-heading')) { if (closestEl && closestEl.classList.contains(style.panelHeading)) {
return closestEl; return closestEl;
} }
return undefined; return undefined;
@@ -14,6 +16,7 @@ function getClosestHeading(el: Element) {
* and odd index element becomes the expandable content. * and odd index element becomes the expandable content.
*/ */
export default class MultiPanel extends HTMLElement { export default class MultiPanel extends HTMLElement {
static get observedAttributes() { return [openOneOnlyAttr]; }
constructor() { constructor() {
super(); super();
@@ -31,12 +34,18 @@ export default class MultiPanel extends HTMLElement {
this._childrenChange(); this._childrenChange();
} }
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (name === openOneOnlyAttr && newValue === null) {
// TODO
}
}
// Click event handler // Click event handler
private _onClick(event: MouseEvent) { private _onClick(event: MouseEvent) {
const el = event.target as Element; const el = event.target as Element;
const heading = getClosestHeading(el); const heading = getClosestHeading(el);
if (!heading) return; if (!heading) return;
this._expand(heading); this._toggle(heading);
} }
// KeyDown event handler // KeyDown event handler
@@ -77,7 +86,7 @@ export default class MultiPanel extends HTMLElement {
case 'Enter': case 'Enter':
case ' ': case ' ':
case 'Spacebar': case 'Spacebar':
this._expand(heading); this._toggle(heading);
break; break;
// Any other key press is ignored and passed back to the browser. // Any other key press is ignored and passed back to the browser.
@@ -93,7 +102,7 @@ export default class MultiPanel extends HTMLElement {
} }
} }
private _expand(heading: Element) { private _toggle(heading: Element) {
if (!heading) return; if (!heading) return;
const content = heading.nextElementSibling; const content = heading.nextElementSibling;
@@ -105,11 +114,21 @@ export default class MultiPanel extends HTMLElement {
content.removeAttribute('expanded'); content.removeAttribute('expanded');
content.setAttribute('aria-expanded', 'false'); content.setAttribute('aria-expanded', 'false');
} else { } else {
if (this.openOneOnly) this._closeAll();
content.setAttribute('expanded', ''); content.setAttribute('expanded', '');
content.setAttribute('aria-expanded', 'true'); content.setAttribute('aria-expanded', 'true');
} }
} }
private _closeAll(): void {
const els = [...this.children].filter(el => el.matches('[expanded]'));
for (const el of els) {
el.removeAttribute('expanded');
el.setAttribute('aria-expanded', 'false');
}
}
// children of multi-panel should always be even number (heading/content pair) // children of multi-panel should always be even number (heading/content pair)
private _childrenChange() { private _childrenChange() {
let preserveTabIndex : boolean = false; let preserveTabIndex : boolean = false;
@@ -130,10 +149,10 @@ export default class MultiPanel extends HTMLElement {
// When odd number of elements were inserted in the middle, // When odd number of elements were inserted in the middle,
// what was heading before may become content after the insertion. // what was heading before may become content after the insertion.
// Remove classes and attributes to prepare for this change. // Remove classes and attributes to prepare for this change.
heading.classList.remove('panel-content'); heading.classList.remove(style.panelContent);
if (content.classList.contains('panel-heading')) { if (content.classList.contains(style.panelHeading)) {
content.classList.remove('panel-heading'); content.classList.remove(style.panelHeading);
} }
if (heading.hasAttribute('expanded') && heading.hasAttribute('aria-expanded')) { if (heading.hasAttribute('expanded') && heading.hasAttribute('aria-expanded')) {
heading.removeAttribute('expanded'); heading.removeAttribute('expanded');
@@ -146,8 +165,8 @@ export default class MultiPanel extends HTMLElement {
} }
// Assign heading and content classes // Assign heading and content classes
heading.classList.add('panel-heading'); heading.classList.add(style.panelHeading);
content.classList.add('panel-content'); content.classList.add(style.panelContent);
// Assign ids and aria-X for heading/content pair. // Assign ids and aria-X for heading/content pair.
heading.id = `panel-heading-${randomId}`; heading.id = `panel-heading-${randomId}`;
@@ -208,7 +227,7 @@ export default class MultiPanel extends HTMLElement {
private _lastHeading() { private _lastHeading() {
// if the last element is heading, return last element // if the last element is heading, return last element
const lastEl = this.lastElementChild as HTMLElement; const lastEl = this.lastElementChild as HTMLElement;
if (lastEl && lastEl.classList.contains('panel-heading')) { if (lastEl && lastEl.classList.contains(style.panelHeading)) {
return lastEl; return lastEl;
} }
// otherwise return 2nd from the last // otherwise return 2nd from the last
@@ -217,6 +236,21 @@ export default class MultiPanel extends HTMLElement {
return lastContent.previousElementSibling as HTMLElement; return lastContent.previousElementSibling as HTMLElement;
} }
} }
/**
* If true, only one panel can be open at once. When one opens, others close.
*/
get openOneOnly() {
return this.hasAttribute(openOneOnlyAttr);
}
set openOneOnly(val: boolean) {
if (val) {
this.setAttribute(openOneOnlyAttr, '');
} else {
this.removeAttribute(openOneOnlyAttr);
}
}
} }
customElements.define('multi-panel', MultiPanel); customElements.define('multi-panel', MultiPanel);

View File

@@ -0,0 +1,9 @@
interface MultiPanelAttributes extends JSX.HTMLAttributes {
'open-one-only'?: boolean;
}
declare namespace JSX {
interface IntrinsicElements {
'multi-panel': MultiPanelAttributes;
}
}

View File

@@ -0,0 +1,11 @@
.panel-heading {
background:gray;
}
.panel-content {
height:0px;
overflow:scroll;
transition: height 1s;
}
.panel-content[expanded] {
height:auto;
}

View File

@@ -24,16 +24,15 @@ import {
EncoderOptions, EncoderOptions,
encoderMap, encoderMap,
} from '../../codecs/encoders'; } from '../../codecs/encoders';
import { import {
PreprocessorState, PreprocessorState,
defaultPreprocessorState, defaultPreprocessorState,
} from '../../codecs/preprocessors'; } from '../../codecs/preprocessors';
import { decodeImage } from '../../codecs/decoders'; import { decodeImage } from '../../codecs/decoders';
import { cleanMerge, cleanSet } from '../../lib/clean-modify'; import { cleanMerge, cleanSet } from '../../lib/clean-modify';
import Processor from '../../codecs/processor'; import Processor from '../../codecs/processor';
import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta'; import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/processor-meta';
import './custom-els/MultiPanel';
export interface SourceImage { export interface SourceImage {
file: File | Fileish; file: File | Fileish;
@@ -397,6 +396,23 @@ export default class Compress extends Component<Props, State> {
const [leftImage, rightImage] = images; const [leftImage, rightImage] = images;
const [leftImageData, rightImageData] = images.map(i => i.data); const [leftImageData, rightImageData] = images.map(i => i.data);
const options = images.map((image, index) => (
<Options
loading={loading || image.loading}
source={source}
mobileView={mobileView}
imageIndex={index}
imageFile={image.file}
downloadUrl={image.downloadUrl}
preprocessorState={image.preprocessorState}
encoderState={image.encoderState}
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index)}
/>
));
return ( return (
<div class={style.compress}> <div class={style.compress}>
<Output <Output
@@ -407,22 +423,16 @@ export default class Compress extends Component<Props, State> {
leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'} leftImgContain={leftImage.preprocessorState.resize.fitMethod === 'cover'}
rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'} rightImgContain={rightImage.preprocessorState.resize.fitMethod === 'cover'}
/> />
{images.map((image, index) => ( {mobileView
<Options ? (
loading={loading || image.loading} <multi-panel class={style.multiPanel} open-one-only>
source={source} <div>Top</div>
mobileView={mobileView} {options[0]}
imageIndex={index} <div>Bottom</div>
imageFile={image.file} {options[1]}
downloadUrl={image.downloadUrl} </multi-panel>
preprocessorState={image.preprocessorState} ) : options
encoderState={image.encoderState} }
onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
onCopyToOtherClick={this.onCopyToOtherClick.bind(this, index)}
/>
))}
</div> </div>
); );
} }

View File

@@ -16,3 +16,7 @@
grid-template-rows: 100%; grid-template-rows: 100%;
} }
} }
.multi-panel {
position: relative;
}

View File

@@ -1,11 +0,0 @@
multi-panel > .panel-heading {
background:gray;
}
multi-panel > .panel-content {
height:0px;
overflow:scroll;
transition: height 1s;
}
multi-panel > .panel-content[expanded] {
height:auto;
}