Image manager: supported a tabbed interface on mobile

Makes interface relatively usable now on mobile sizes.
Required updating of tab handling to support tabs being active at only
mobile screen sizes, include change on resize, upon support for
potentially nested tab usage.
Tab component will now search within sensible depths for finding its own
tabs and panels to control.
This commit is contained in:
Dan Brown 2023-05-27 16:58:10 +01:00
parent dc6133c4c4
commit 946c9ae804
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
4 changed files with 139 additions and 61 deletions

View file

@ -21,15 +21,23 @@ export class Tabs extends Component {
setup() {
this.container = this.$el;
this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
this.tabList = this.container.querySelector('[role="tablist"]');
this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]'));
this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]'));
this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000;
this.active = null;
this.container.addEventListener('click', event => {
const button = event.target.closest('[role="tab"]');
if (button) {
this.show(button.getAttribute('aria-controls'));
const tab = event.target.closest('[role="tab"]');
if (tab && this.tabs.includes(tab)) {
this.show(tab.getAttribute('aria-controls'));
}
});
window.addEventListener('resize', this.updateActiveState.bind(this), {
passive: true,
});
this.updateActiveState();
}
show(sectionId) {
@ -46,4 +54,34 @@ export class Tabs extends Component {
this.$emit('change', {showing: sectionId});
}
updateActiveState() {
const active = window.innerWidth < this.activeUnder;
if (active === this.active) {
return;
}
if (active) {
this.activate();
} else {
this.deactivate();
}
this.active = active;
}
activate() {
this.show(this.panels[0].id);
this.tabList.toggleAttribute('hidden', false);
}
deactivate() {
for (const panel of this.panels) {
panel.removeAttribute('hidden');
}
for (const tab of this.tabs) {
tab.setAttribute('aria-selected', 'false');
}
this.tabList.toggleAttribute('hidden', true);
}
}

View file

@ -380,6 +380,12 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
background-color: #FFF;
}
}
@include smaller-than($s) {
.image-manager-filter-bar .contained-search-box input {
width: 180px;
}
}
.image-manager-filters {
box-shadow: $bs-med;
border-radius: 4px;
@ -475,6 +481,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-manager-sidebar {
width: 300px;
margin: 0 auto;
overflow-y: auto;
overflow-x: hidden;
border-inline-start: 1px solid #DDD;
@ -500,6 +507,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
}
}
}
@include smaller-than($m) {
.image-manager-sidebar {
border-inline-start: 0;
}
}
.image-manager-content {
display: flex;

View file

@ -298,6 +298,10 @@ body.flexbox {
}
}
[hidden] {
display: none !important;
}
/**
* Border radiuses
*/

View file

@ -24,67 +24,91 @@
<button refs="popup@hide" type="button" class="popup-header-close">@icon('close')</button>
</div>
<div refs="dropzone@drop-target" class="flex-fill image-manager-body">
<div class="image-manager-content">
<div class="image-manager-filter-bar flex-container-row justify-space-between">
<div class="primary-background image-manager-filter-bar-bg"></div>
<div>
<form refs="image-manager@searchForm" class="contained-search-box">
<input refs="image-manager@searchInput"
placeholder="{{ trans('components.image_search_hint') }}"
type="text">
<button refs="image-manager@cancelSearch"
title="{{ trans('common.search_clear') }}"
type="button"
class="cancel">@icon('close')</button>
<button type="submit"
title="{{ trans('common.search') }}">@icon('search')</button>
</form>
</div>
<div class="tab-container bordered tab-primary">
<div role="tablist" class="image-manager-filters flex-container-row">
<button refs="image-manager@filterTabs"
data-filter="all"
role="tab"
aria-selected="true"
type="button"
title="{{ trans('components.image_all_title') }}">@icon('images')</button>
<button refs="image-manager@filterTabs"
data-filter="book"
role="tab"
aria-selected="false"
type="button"
title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon'])</button>
<button refs="image-manager@filterTabs"
data-filter="page"
role="tab"
aria-selected="false"
type="button"
title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon'])</button>
<div component="tabs"
option:tabs:active-under="880"
refs="dropzone@drop-target"
class="flex-container-column image-manager-body">
<div class="tab-container">
<div role="tablist" class="hide-over-m mb-none">
<button id="image-manager-list-tab"
aria-selected="true"
aria-controls="image-manager-list"
role="tab">Image List</button>
<button id="image-manager-info-tab"
aria-selected="true"
aria-controls="image-manager-info"
role="tab">Image Details</button>
</div>
</div>
<div class="flex-container-row flex-fill">
<div id="image-manager-list"
tabindex="0"
role="tabpanel"
aria-labelledby="image-manager-list-tab"
class="image-manager-content">
<div class="image-manager-filter-bar flex-container-row wrap justify-space-between">
<div class="primary-background image-manager-filter-bar-bg"></div>
<div>
<form refs="image-manager@searchForm" class="contained-search-box">
<input refs="image-manager@searchInput"
placeholder="{{ trans('components.image_search_hint') }}"
type="text">
<button refs="image-manager@cancelSearch"
title="{{ trans('common.search_clear') }}"
type="button"
class="cancel">@icon('close')</button>
<button type="submit"
title="{{ trans('common.search') }}">@icon('search')</button>
</form>
</div>
<div class="tab-container bordered tab-primary">
<div role="tablist" class="image-manager-filters flex-container-row">
<button refs="image-manager@filterTabs"
data-filter="all"
role="tab"
aria-selected="true"
type="button"
title="{{ trans('components.image_all_title') }}">@icon('images')</button>
<button refs="image-manager@filterTabs"
data-filter="book"
role="tab"
aria-selected="false"
type="button"
title="{{ trans('components.image_book_title') }}">@icon('book', ['class' => 'svg-icon'])</button>
<button refs="image-manager@filterTabs"
data-filter="page"
role="tab"
aria-selected="false"
type="button"
title="{{ trans('components.image_page_title') }}">@icon('page', ['class' => 'svg-icon'])</button>
</div>
</div>
</div>
<div refs="image-manager@listContainer" class="image-manager-list"></div>
<div refs="image-manager@loadMore" class="load-more" hidden>
<button type="button" class="button small outline">Load More</button>
</div>
</div>
<div refs="image-manager@listContainer" class="image-manager-list"></div>
<div refs="image-manager@loadMore" class="load-more" hidden>
<button type="button" class="button small outline">Load More</button>
<div id="image-manager-info"
tabindex="0"
role="tabpanel"
aria-labelledby="image-manager-info-tab"
class="image-manager-sidebar flex-container-column">
<div refs="image-manager@dropzoneContainer">
<div refs="dropzone@status-area"></div>
</div>
<div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
<p>{{ trans('components.image_intro') }}</p>
<p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
</div>
<div refs="image-manager@formContainer" class="inner flex">
</div>
</div>
</div>
<div class="image-manager-sidebar flex-container-column">
<div refs="image-manager@dropzoneContainer">
<div refs="dropzone@status-area"></div>
</div>
<div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
<p>{{ trans('components.image_intro') }}</p>
<p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
</div>
<div refs="image-manager@formContainer" class="inner flex">
</div>
</div>
</div>
<div class="popup-footer">