mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
refactor: more elements (#22095)
This commit is contained in:
parent
3f4b6a8e7c
commit
75322179fd
25 changed files with 51 additions and 72 deletions
|
|
@ -1,78 +0,0 @@
|
|||
import StarRating from '$lib/components/shared-components/star-rating.svelte';
|
||||
import { render } from '@testing-library/svelte';
|
||||
|
||||
describe('StarRating component', () => {
|
||||
it('renders correctly', () => {
|
||||
const component = render(StarRating, {
|
||||
count: 3,
|
||||
rating: 2,
|
||||
readOnly: false,
|
||||
onRating: vi.fn(),
|
||||
});
|
||||
const container = component.getByTestId('star-container') as HTMLImageElement;
|
||||
expect(container.className).toBe('flex flex-row');
|
||||
|
||||
const radioButtons = component.getAllByRole('radio') as HTMLInputElement[];
|
||||
expect(radioButtons.length).toBe(3);
|
||||
const labels = component.getAllByTestId('star') as HTMLLabelElement[];
|
||||
expect(labels.length).toBe(3);
|
||||
const labelText = component.getAllByText('rating_count') as HTMLSpanElement[];
|
||||
expect(labelText.length).toBe(3);
|
||||
const clearButton = component.getByRole('button') as HTMLButtonElement;
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
|
||||
// Check the clear button content
|
||||
expect(clearButton.textContent).toBe('rating_clear');
|
||||
|
||||
// Check the initial state
|
||||
expect(radioButtons[0].checked).toBe(false);
|
||||
expect(radioButtons[1].checked).toBe(true);
|
||||
expect(radioButtons[2].checked).toBe(false);
|
||||
|
||||
// Check the radio button attributes
|
||||
for (const [index, radioButton] of radioButtons.entries()) {
|
||||
expect(radioButton.id).toBe(labels[index].htmlFor);
|
||||
expect(radioButton.name).toBe('stars');
|
||||
expect(radioButton.value).toBe((index + 1).toString());
|
||||
expect(radioButton.disabled).toBe(false);
|
||||
expect(radioButton.className).toBe('sr-only');
|
||||
}
|
||||
|
||||
// Check the label attributes
|
||||
for (const label of labels) {
|
||||
expect(label.className).toBe('cursor-pointer');
|
||||
expect(label.tabIndex).toBe(-1);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders correctly with readOnly', () => {
|
||||
const component = render(StarRating, {
|
||||
count: 3,
|
||||
rating: 2,
|
||||
readOnly: true,
|
||||
onRating: vi.fn(),
|
||||
});
|
||||
const radioButtons = component.getAllByRole('radio') as HTMLInputElement[];
|
||||
expect(radioButtons.length).toBe(3);
|
||||
const labels = component.getAllByTestId('star') as HTMLLabelElement[];
|
||||
expect(labels.length).toBe(3);
|
||||
const clearButton = component.queryByRole('button');
|
||||
expect(clearButton).toBeNull();
|
||||
|
||||
// Check the initial state
|
||||
expect(radioButtons[0].checked).toBe(false);
|
||||
expect(radioButtons[1].checked).toBe(true);
|
||||
expect(radioButtons[2].checked).toBe(false);
|
||||
|
||||
// Check the radio button attributes
|
||||
for (const [index, radioButton] of radioButtons.entries()) {
|
||||
expect(radioButton.id).toBe(labels[index].htmlFor);
|
||||
expect(radioButton.disabled).toBe(true);
|
||||
}
|
||||
|
||||
// Check the label attributes
|
||||
for (const label of labels) {
|
||||
expect(label.className).toBe('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import Icon from '$lib/elements/Icon.svelte';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
rating: number;
|
||||
readOnly?: boolean;
|
||||
onRating: (rating: number) => void | undefined;
|
||||
}
|
||||
|
||||
let { count = 5, rating, readOnly = false, onRating }: Props = $props();
|
||||
|
||||
let ratingSelection = $derived(rating);
|
||||
let hoverRating = $state(0);
|
||||
let focusRating = $state(0);
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const starIcon =
|
||||
'M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z';
|
||||
const id = generateId();
|
||||
|
||||
const handleSelect = (newRating: number) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newRating === rating) {
|
||||
return;
|
||||
}
|
||||
|
||||
onRating(newRating);
|
||||
};
|
||||
|
||||
const setHoverRating = (value: number) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
hoverRating = value;
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setHoverRating(0);
|
||||
focusRating = 0;
|
||||
};
|
||||
|
||||
const handleSelectDebounced = (value: number) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
handleSelect(value);
|
||||
}, 300);
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||
<fieldset
|
||||
class="text-immich-primary dark:text-immich-dark-primary w-fit cursor-default"
|
||||
onmouseleave={() => setHoverRating(0)}
|
||||
use:focusOutside={{ onFocusOut: reset }}
|
||||
use:shortcuts={[
|
||||
{ shortcut: { key: 'ArrowLeft' }, preventDefault: false, onShortcut: (event) => event.stopPropagation() },
|
||||
{ shortcut: { key: 'ArrowRight' }, preventDefault: false, onShortcut: (event) => event.stopPropagation() },
|
||||
]}
|
||||
>
|
||||
<legend class="sr-only">{$t('rating')}</legend>
|
||||
<div class="flex flex-row" data-testid="star-container">
|
||||
{#each { length: count } as _, index (index)}
|
||||
{@const value = index + 1}
|
||||
{@const filled = hoverRating >= value || (hoverRating === 0 && ratingSelection >= value)}
|
||||
{@const starId = `${id}-${value}`}
|
||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<label
|
||||
for={starId}
|
||||
class:cursor-pointer={!readOnly}
|
||||
class:ring-2={focusRating === value}
|
||||
onmouseover={() => setHoverRating(value)}
|
||||
tabindex={-1}
|
||||
data-testid="star"
|
||||
>
|
||||
<span class="sr-only">{$t('rating_count', { values: { count: value } })}</span>
|
||||
<Icon
|
||||
path={starIcon}
|
||||
size="1.5em"
|
||||
strokeWidth={1}
|
||||
color={filled ? 'currentcolor' : 'transparent'}
|
||||
strokeColor={filled ? 'currentcolor' : '#c1cce8'}
|
||||
ariaHidden
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="stars"
|
||||
{value}
|
||||
id={starId}
|
||||
bind:group={ratingSelection}
|
||||
disabled={readOnly}
|
||||
onfocus={() => {
|
||||
focusRating = value;
|
||||
}}
|
||||
onchange={() => handleSelectDebounced(value)}
|
||||
class="sr-only"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
{#if ratingSelection > 0 && !readOnly}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
ratingSelection = 0;
|
||||
handleSelect(ratingSelection);
|
||||
}}
|
||||
class="cursor-pointer text-xs text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
{$t('rating_clear')}
|
||||
</button>
|
||||
{/if}
|
||||
Loading…
Add table
Add a link
Reference in a new issue