Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/js/components/assets/Browser/Browser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@

<div class="flex items-center gap-2 sm:gap-3 py-3 relative overflow-clip st-overflow-clip-margin">
<div class="flex flex-1 items-center gap-2 sm:gap-3">
<ListingSearch />
<ListingSearch :label="__('Search assets')" />
<ListingFilters @filters-updated="filtersUpdated" />
</div>
<ListingCustomizeColumns v-if="mode === 'table'" />
Expand Down
2 changes: 1 addition & 1 deletion resources/js/components/assets/Selector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
<template #header="{ canUpload, openFileBrowser, canCreateFolders, startCreatingFolder, mode, modeChanged }">
<div class="flex items-center gap-2 sm:gap-3 mb-4">
<div class="flex flex-1 items-center gap-2 sm:gap-3">
<Search ref="search" />
<Search ref="search" :label="__('Search assets')" />
</div>

<Button v-if="canUpload" :text="__('Upload')" icon="upload" @click="openFileBrowser" />
Expand Down
12 changes: 9 additions & 3 deletions resources/js/components/ui/Listing/Search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
import { injectListingContext } from '../Listing/Listing.vue';
import { Input } from '@ui';
import debounce from '@/util/debounce.js';
import { useTemplateRef } from 'vue';
import { useId, useTemplateRef } from 'vue';

defineProps({
/** Accessible label for the search field. Pass the listing's own noun, e.g. "Search assets". */
label: { type: String, default: null },
});

const id = useId();
const { activeFilterBadgeCount, searchQuery, setSearchQuery, reorderable } = injectListingContext();
const searchQueryUpdated = debounce((value) => setSearchQuery(value), 300);

Expand All @@ -15,12 +21,12 @@ defineExpose({ focus });

<template>
<div class="flex-1 max-w-sm" :class="{ 'max-w-60!': activeFilterBadgeCount > 2 }">
<label for="listings-search" class="sr-only">{{ __('Search entries') }}</label>
<label :for="id" class="sr-only">{{ label ? __(label) : __('Search') }}</label>
<Input
:focus="true"
ref="input"
icon="magnifying-glass"
id="listings-search"
:id="id"
variant="light"
clearable
:placeholder="__('Search...')"
Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/ui/Listing/TableHead.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ const hasVisibleHeader = computed(() => {
scope="col"
>
<ToggleAll v-if="allowsSelections && allowsMultipleSelections" />
<span v-else class="sr-only">{{ reorderable ? __('Reorder') : __('Select') }}</span>
</th>
<HeaderCell v-for="column in visibleColumns" :key="column.field" :column :data-column="column.field" />
<!-- <th class="type-column" v-if="type">
<template v-if="type === 'entries'">{{ __('Collection') }}</template>
<template v-if="type === 'terms'">{{ __('Taxonomy') }}</template>
</th> -->
<th scope="col" class="actions-column" v-if="hasActions" />
<th scope="col" class="actions-column" v-if="hasActions">
<span class="sr-only">{{ __('Actions') }}</span>
</th>
</tr>
</thead>

Expand Down
59 changes: 59 additions & 0 deletions resources/js/tests/components/ListingSearch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { mount } from '@vue/test-utils';
import { expect, test } from 'vitest';
import { h } from 'vue';
import * as Globals from '@/bootstrap/globals';
import Listing from '@/components/ui/Listing/Listing.vue';
import Search from '@/components/ui/Listing/Search.vue';
import TableHead from '@/components/ui/Listing/TableHead.vue';

Object.keys(Globals).forEach((fn) => (window[fn] = Globals[fn]));

window.Statamic = {
$config: { get: () => undefined },
$progress: { loading: () => {}, complete: () => {} },
$preferences: { get: () => undefined },
$events: { $on: () => {}, $off: () => {}, $emit: () => {} },
};

const mountInListing = (child, listingProps = {}) =>
mount(Listing, { props: { items: [], ...listingProps }, slots: { default: () => child } });

test('the search label defaults to a listing-agnostic string', () => {
const wrapper = mountInListing(h(Search));

expect(wrapper.find('label').text()).toBe('Search');
});

test('the search label can be set per listing', () => {
const wrapper = mountInListing(h(Search, { label: 'Search assets' }));

expect(wrapper.find('label').text()).toBe('Search assets');
});

test('the search label is associated with the input', () => {
const wrapper = mountInListing(h(Search));
const id = wrapper.find('input').attributes('id');

expect(id).toBeTruthy();
expect(wrapper.find('label').attributes('for')).toBe(id);
});

test('two search fields in the same listing do not share an id', () => {
const wrapper = mountInListing(h('div', [h(Search), h(Search)]));
const [a, b] = wrapper.findAll('input').map((input) => input.attributes('id'));

expect(a).toBeTruthy();
expect(b).toBeTruthy();
expect(a).not.toBe(b);
});

test('the actions header cell has an accessible name', () => {
const wrapper = mountInListing(h(TableHead), {
columns: [{ field: 'title', label: 'Title' }],
actionUrl: '/cp/collections/pages/actions',
});
const actionsHeader = wrapper.find('th.actions-column');

expect(actionsHeader.exists()).toBe(true);
expect(actionsHeader.text()).toBe('Actions');
});
Loading