@use 'sprucecss/scss/spruce' as *;
.file-list {
@include generate-variables($form-control, $include: ('border-width', 'border-radius'));
display: flex;
flex-direction: column;
gap: spacer('s');
&__items {
@include clear-list;
border: config('border-width', $form-control) solid color('border', 'form');
border-radius: config('border-radius', $form-control);
padding: spacer('xs');
> li + li {
border-block-start: config('border-width', $form-control) solid color('border');
margin-block-start: spacer('xs');
padding-block-start: spacer('xs');
}
}
}
.file-list-item {
align-items: center;
display: flex;
gap: spacer('s');
justify-content: space-between;
&__icon {
align-items: center;
aspect-ratio: 1;
background-color: color-alpha('primary', 0.08);
border-radius: radius('sm');
display: flex;
inline-size: 2.5rem;
justify-content: center;
svg {
--size: 1rem;
block-size: var(--size);
color: color('primary');
inline-size: var(--size);
}
}
&__column {
align-items: center;
display: flex;
gap: spacer('s');
}
&__thumbnail {
aspect-ratio: 1;
border-radius: radius('sm');
inline-size: 2.5rem;
object-fit: cover;
}
&__name {
@include text-ellipsis(1);
max-inline-size: 10ch;
@include breakpoint('xs') {
max-inline-size: 25ch;
}
}
&__actions {
align-items: center;
display: flex;
gap: spacer('xxs');
}
}
<div class="form-group">
<span class="form-label" id="gallery-label">Gallery</span>
<div class="file-list" data-file-list>
<button type="button" class="btn btn--primary btn--block">Choose images</button>
<ul class="file-list__items" aria-labelledby="gallery-label">
<li class="file-list-item">
<div class="file-list-item__column">
<img class="file-list-item__thumbnail" src="https://picsum.photos/80/80" alt=""/>
<span class="file-list-item__name" id="list-item-1">pic1234.jpg</span>
</div>
<div class="file-list-item__actions">
<button type="button" class="btn btn--light btn--sm btn--icon" aria-describedby="list-item-1" aria-label="Move up" data-file-list-move="-1" disabled>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<button type="button" class="btn btn--light btn--sm btn--icon" aria-describedby="list-item-1" aria-label="Move down" data-file-list-move="1">
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<button type="button" class="btn btn--soft-danger btn--sm btn--icon" aria-describedby="list-item-1" aria-label="Remove" data-file-list-remove>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</li>
<li class="file-list-item">
<div class="file-list-item__column">
<span class="file-list-item__icon">
<svg aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
</span>
<span class="file-list-item__name" id="list-item-2">document5678.pdf</span>
</div>
<div class="file-list-item__actions">
<button type="button" class="btn btn--light btn--sm btn--icon" aria-describedby="list-item-2" aria-label="Move up" data-file-list-move="-1">
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<button type="button" class="btn btn--light btn--sm btn--icon" aria-describedby="list-item-2" aria-label="Move down" data-file-list-move="1">
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<button type="button" class="btn btn--soft-danger btn--sm btn--icon" aria-describedby="list-item-2" aria-label="Remove" data-file-list-remove>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</li>
<li class="file-list-item">
<div class="file-list-item__column">
<img class="file-list-item__thumbnail" src="https://picsum.photos/81/81" alt=""/>
<span class="file-list-item__name" id="list-item-3">image5678.png</span>
</div>
<div class="file-list-item__actions">
<button type="button" class="btn btn--light btn--sm btn--icon" aria-describedby="list-item-3" aria-label="Move up" data-file-list-move="-1">
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<button type="button" class="btn btn--light btn--sm btn--icon" aria-describedby="list-item-3" aria-label="Move down" data-file-list-move="1" disabled>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<button type="button" class="btn btn--soft-danger btn--sm btn--icon" aria-describedby="list-item-3" aria-label="Remove" data-file-list-remove>
<svg class="btn__icon" aria-hidden="true" fill="none" focusable="false" height="24" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5" stroke="currentColor" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</li>
</ul>
<p class="sr-only" aria-live="polite" data-file-list-status></p>
</div>
</div>
document.querySelectorAll('[data-file-list]').forEach((fileList) => {
const list = fileList.querySelector('ul');
const status = fileList.querySelector('[data-file-list-status]');
const nameOf = (item) => item.querySelector('.file-list-item__name').textContent.trim();
const update = () => {
const items = [...list.children];
items.forEach((item, index) => {
item.querySelector('[data-file-list-move="-1"]').disabled = index === 0;
item.querySelector('[data-file-list-move="1"]').disabled = index === items.length - 1;
});
};
list.addEventListener('click', (event) => {
const button = event.target.closest('button');
const item = button?.closest('li');
if (!item) return;
if (button.hasAttribute('data-file-list-remove')) {
const next = item.nextElementSibling ?? item.previousElementSibling;
item.remove();
status.textContent = `${nameOf(item)} removed.`;
next?.querySelector('[data-file-list-remove]').focus();
} else if (button.dataset.fileListMove) {
const up = button.dataset.fileListMove === '-1';
up ? item.previousElementSibling.before(item) : item.nextElementSibling.after(item);
update();
(button.disabled ? item.querySelector(`[data-file-list-move="${up ? 1 : -1}"]`) : button).focus();
status.textContent = `${nameOf(item)} moved to position ${[...list.children].indexOf(item) + 1} of ${list.children.length}.`;
}
update();
});
update();
});