Skip to content

Commit 73552e5

Browse files
authored
Merge pull request #2538 from appwrite/add-arrow-navs
2 parents f653a82 + 4d4bac8 commit 73552e5

File tree

5 files changed

+145
-12
lines changed

5 files changed

+145
-12
lines changed

src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/+layout.svelte

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,21 @@
4747
import { page } from '$app/state';
4848
import { base } from '$app/paths';
4949
import { canWriteTables } from '$lib/stores/roles';
50-
import { IconEye, IconLockClosed, IconPlus, IconPuzzle } from '@appwrite.io/pink-icons-svelte';
50+
import {
51+
IconChevronDown,
52+
IconChevronUp,
53+
IconEye,
54+
IconLockClosed,
55+
IconPlus,
56+
IconPuzzle
57+
} from '@appwrite.io/pink-icons-svelte';
5158
import SideSheet from './layout/sidesheet.svelte';
5259
import EditRow from './rows/edit.svelte';
5360
import EditRelatedRow from './rows/editRelated.svelte';
5461
import EditColumn from './columns/edit.svelte';
5562
import RowActivity from './rows/activity.svelte';
5663
import EditRowPermissions from './rows/editPermissions.svelte';
57-
import { Dialog, Layout, Typography, Selector } from '@appwrite.io/pink-svelte';
64+
import { Dialog, Layout, Typography, Selector, Icon } from '@appwrite.io/pink-svelte';
5865
import { Button, Seekbar } from '$lib/elements/forms';
5966
import { generateFakeRecords, generateColumns } from '$lib/helpers/faker';
6067
import { addNotification } from '$lib/stores/notifications';
@@ -65,6 +72,7 @@
6572
import { chunks } from '$lib/helpers/array';
6673
import { Submit, trackEvent } from '$lib/actions/analytics';
6774
75+
import { isTabletViewport } from '$lib/stores/viewport';
6876
import IndexesSuggestions from '../(suggestions)/indexes.svelte';
6977
7078
let editRow: EditRow;
@@ -78,6 +86,33 @@
7886
7987
let columnCreationHandler: ((response: RealtimeResponse) => void) | null = null;
8088
89+
// manual management of focus is needed!
90+
const autoFocusAction = (node: HTMLElement, shouldFocus: boolean) => {
91+
const button = node.querySelector('button');
92+
if (!button) return;
93+
94+
const handleBlur = () => button.classList.remove('focus-visible');
95+
const applyFocus = (focus: boolean) => {
96+
if (focus) {
97+
button.classList.add('focus-visible');
98+
button.focus();
99+
} else {
100+
button.classList.remove('focus-visible');
101+
}
102+
};
103+
104+
button.addEventListener('blur', handleBlur);
105+
applyFocus(shouldFocus);
106+
107+
return {
108+
update: applyFocus,
109+
destroy() {
110+
button.removeEventListener('blur', handleBlur);
111+
button.classList.remove('focus-visible');
112+
}
113+
};
114+
};
115+
81116
onMount(() => {
82117
expandTabs.set(preferences.getKey('tableHeaderExpanded', true));
83118
@@ -448,11 +483,63 @@
448483
show: !!currentRowId,
449484
value: buildRowUrl(currentRowId)
450485
}}>
486+
{#snippet topEndActions()}
487+
{@const rows = $databaseRowSheetOptions.rows ?? []}
488+
{@const currentIndex = $databaseRowSheetOptions.rowIndex ?? -1}
489+
{@const isFirstRow = currentIndex <= 0}
490+
{@const isLastRow = currentIndex >= rows.length - 1}
491+
492+
{#if !$isTabletViewport}
493+
{@const shouldFocusPrev = !$databaseRowSheetOptions.autoFocus && !isFirstRow}
494+
{@const shouldFocusNext =
495+
!$databaseRowSheetOptions.autoFocus && isFirstRow && !isLastRow}
496+
497+
<div use:autoFocusAction={shouldFocusPrev} class:nav-button-wrapper={shouldFocusPrev}>
498+
<Button
499+
icon
500+
text
501+
size="xs"
502+
on:click={() => {
503+
if (currentIndex > 0) {
504+
databaseRowSheetOptions.update((opts) => ({
505+
...opts,
506+
row: rows[currentIndex - 1],
507+
rowIndex: currentIndex - 1
508+
}));
509+
}
510+
}}
511+
disabled={isFirstRow}>
512+
<Icon icon={IconChevronUp} />
513+
</Button>
514+
</div>
515+
516+
<div use:autoFocusAction={shouldFocusNext} class:nav-button-wrapper={shouldFocusNext}>
517+
<Button
518+
icon
519+
text
520+
size="xs"
521+
on:click={() => {
522+
if (currentIndex < rows.length - 1) {
523+
databaseRowSheetOptions.update((opts) => ({
524+
...opts,
525+
row: rows[currentIndex + 1],
526+
rowIndex: currentIndex + 1
527+
}));
528+
}
529+
}}
530+
disabled={isLastRow}>
531+
<Icon icon={IconChevronDown} />
532+
</Button>
533+
</div>
534+
{/if}
535+
{/snippet}
536+
451537
{#key currentRowId}
452538
<EditRow
453539
bind:this={editRow}
454540
bind:row={$databaseRowSheetOptions.row}
455-
bind:rowId={$databaseRowSheetOptions.rowId} />
541+
bind:rowId={$databaseRowSheetOptions.rowId}
542+
autoFocus={$databaseRowSheetOptions.autoFocus} />
456543
{/key}
457544
</SideSheet>
458545

@@ -522,3 +609,10 @@
522609
</Dialog>
523610

524611
<IndexesSuggestions />
612+
613+
<style lang="scss">
614+
// not the best solution but needed!
615+
.nav-button-wrapper :global(button.focus-visible) {
616+
outline: var(--border-width-l) solid var(--border-focus);
617+
}
618+
</style>

src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/layout/sidesheet.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
footer = null,
1919
titleBadge = null,
2020
topAction = null,
21+
topEndActions = null,
2122
...restProps
2223
}: {
2324
show: boolean;
@@ -48,7 +49,8 @@
4849
}
4950
| undefined;
5051
children?: Snippet;
51-
footer?: Snippet | null;
52+
footer?: Snippet;
53+
topEndActions?: Snippet;
5254
} & HTMLAttributes<HTMLDivElement> = $props();
5355
5456
let form: Form;
@@ -88,6 +90,12 @@
8890
{/if}
8991
{/if}
9092
</Layout.Stack>
93+
94+
{#if topEndActions}
95+
<Layout.Stack direction="row" gap="xs" alignItems="center" inline>
96+
{@render topEndActions()}
97+
</Layout.Stack>
98+
{/if}
9199
</Layout.Stack>
92100
</div>
93101

src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/edit.svelte

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,27 @@
1010
import { invalidate } from '$app/navigation';
1111
import { table, type Columns, PROHIBITED_ROW_KEYS } from '../store';
1212
import ColumnItem from './columns/columnItem.svelte';
13-
import { buildWildcardColumnsQuery, isRelationship, isRelationshipToMany } from './store';
13+
import {
14+
buildWildcardColumnsQuery,
15+
isRelationship,
16+
isRelationshipToMany,
17+
isSpatialType
18+
} from './store';
1419
import { Layout, Skeleton } from '@appwrite.io/pink-svelte';
1520
import { deepClone } from '$lib/helpers/object';
21+
import deepEqual from 'deep-equal';
1622
1723
const tableId = page.params.table;
1824
const databaseId = page.params.database;
1925
2026
let {
2127
row = $bindable(),
22-
rowId = $bindable(null)
28+
rowId = $bindable(null),
29+
autoFocus = true
2330
}: {
2431
row?: Models.Row | null;
2532
rowId?: string | null;
33+
autoFocus?: boolean;
2634
} = $props();
2735
2836
let loading = $state(false);
@@ -76,7 +84,9 @@
7684
$effect(() => {
7785
if (row) {
7886
work = initWork();
79-
requestAnimationFrame(() => focusFirstInput());
87+
if (autoFocus) {
88+
requestAnimationFrame(() => focusFirstInput());
89+
}
8090
} else {
8191
work = null;
8292
}
@@ -90,6 +100,10 @@
90100
const workColumn = $work?.[column.key];
91101
const currentColumn = $doc?.[column.key];
92102
103+
if (isSpatialType(column)) {
104+
return deepEqual(workColumn, currentColumn);
105+
}
106+
93107
if (column.array) {
94108
return !symmetricDifference(Array.from(workColumn), Array.from(currentColumn)).length;
95109
}

src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/spreadsheet.svelte

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
spreadsheetRenderKey.set(hash(Date.now().toString()));
113113
}
114114
115+
// create index map for O(1) row lookups, reactive!
116+
$: rowIndexMap = new Map($paginatedRows.items.map((row, index) => [row.$id, index]));
117+
115118
const tableId = page.params.table;
116119
const databaseId = page.params.database;
117120
const organizationId = data.organization.$id ?? data.project.teamId;
@@ -546,11 +549,14 @@
546549
} else if (type === 'row') {
547550
if (action === 'update') {
548551
databaseRowSheetOptions.update((opts) => {
552+
const rowIndex = rowIndexMap.get(row.$id) ?? -1;
549553
return {
550554
...opts,
551555
row,
556+
rowIndex,
552557
show: true,
553-
title: 'Update row'
558+
title: 'Update row',
559+
rows: $paginatedRows.items
554560
};
555561
});
556562
}
@@ -800,9 +806,10 @@
800806
expandKbdShortcut="Cmd+Enter"
801807
on:expandKbdShortcut={({ detail }) => {
802808
const focusedRowId = detail.rowId;
803-
const focusedRow = $rows.rows.find((row) => row.$id === focusedRowId);
809+
const focusedRow = $paginatedRows.items.find((row) => row.$id === focusedRowId);
804810

805811
previouslyFocusedElement = document.activeElement;
812+
$databaseRowSheetOptions.autoFocus = false;
806813
onSelectSheetOption('update', null, 'row', focusedRow);
807814
}}>
808815
<svelte:fragment slot="header" let:root>
@@ -929,6 +936,7 @@
929936
hide();
930937
previouslyFocusedElement =
931938
document.activeElement;
939+
$databaseRowSheetOptions.autoFocus = false;
932940
onSelectSheetOption(
933941
'update',
934942
null,
@@ -979,8 +987,10 @@
979987
<SheetOptions
980988
type="row"
981989
column={rowColumn}
982-
onSelect={(option) =>
983-
onSelectSheetOption(option, null, 'row', row)}
990+
onSelect={(option) => {
991+
$databaseRowSheetOptions.autoFocus = true;
992+
onSelectSheetOption(option, null, 'row', row);
993+
}}
984994
onVisibilityChanged={(visible) => {
985995
canShowDatetimePopover = !visible;
986996
}}>
@@ -1107,6 +1117,7 @@
11071117
rowColumn
11081118
);
11091119
} else {
1120+
$databaseRowSheetOptions.autoFocus = true;
11101121
onSelectSheetOption('update', null, 'row', row);
11111122
}
11121123
}} />

src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/store.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,18 @@ export const databaseRowSheetOptions = writable<
7070
DatabaseSheetOptions & {
7171
row: Models.Row;
7272
rowId?: string;
73+
rows: Models.Row[];
74+
rowIndex?: number;
75+
autoFocus?: boolean;
7376
}
7477
>({
7578
title: null,
7679
show: false,
7780
row: null,
78-
rowId: null // for loading from a given id
81+
rowId: null, // for loading from a given id
82+
rows: [],
83+
rowIndex: -1,
84+
autoFocus: true
7985
});
8086

8187
export const databaseRelatedRowSheetOptions = writable<

0 commit comments

Comments
 (0)