Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a11y support #47

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
46 changes: 41 additions & 5 deletions src/lib/Pane.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
import type { Action } from 'svelte/action';
import { KEY } from './Splitpanes.svelte';
import type { IPane, SplitContext } from '.';

const { onPaneInit, onPaneAdd, onPaneRemove, onPaneClick, isHorizontal, showFirstSplitter, veryFirstPaneKey } =
const { onPaneInit, onPaneAdd, onPaneRemove, onPaneClick, isHorizontal, showFirstSplitter } =
getContext<SplitContext>(KEY);

// PROPS
Expand All @@ -22,6 +23,11 @@
const key = {};
let element: HTMLElement;
let sz: number = size == null ? 0 : size;
const sizeStore = writable({
size,
minSize,
maxSize
});

const isBrowser = typeof window !== 'undefined';

Expand All @@ -31,6 +37,14 @@
sz = size;
}

$: {
sizeStore.update((details) => {
details.minSize = minSize;
details.maxSize = maxSize;
return details;
});
}

$: dimension = $isHorizontal ? 'height' : 'width';

$: style =
Expand All @@ -42,7 +56,8 @@
.filter((value) => value !== undefined)
.join(' ') || undefined;

const { onSplitterDown, onSplitterClick, onSplitterDblClick } = onPaneInit(key);
const result = onPaneInit(key, sizeStore);
const { onSplitterDown, onSplitterClick, onSplitterDblClick, previousPaneSizeStore } = result;

function handleMouseClick(event: MouseEvent) {
onPaneClick(event, key);
Expand Down Expand Up @@ -80,6 +95,10 @@
if (size != null) {
size = sz;
}
sizeStore.update((details) => {
details.size = v;
return details;
});
},
min: () => minSize,
max: () => maxSize,
Expand All @@ -94,12 +113,29 @@
</script>

<!-- Splitter -->
<!-- TODO: Support aria role="separator" and make this a focusable separtor. Sources:
<!-- TODO: Make this a focusable separtor, and check if it is non-focusable (when minSize=maxSize).
Need also:
* Let the user control about aria values (such as aria-valuetext), and if it is focusable.default.render
* Let the user decide which pane he wish to leave without any seperator description, and describe the rest of them.
* Verify it's working in SSR.
* Keybinding
Sources:
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/roles/separator_role
* https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
-->
{#if $veryFirstPaneKey !== key || $showFirstSplitter}
<div use:splitterAction class="splitpanes__splitter" />
{#if $previousPaneSizeStore || $showFirstSplitter}
<div
use:splitterAction
class="splitpanes__splitter"
role={$previousPaneSizeStore ? 'separator' : undefined}
aria-valuenow={$previousPaneSizeStore?.size ?? undefined}
aria-valuemin={$previousPaneSizeStore && $previousPaneSizeStore.minSize > 0
? $previousPaneSizeStore.minSize
: undefined}
aria-valuemax={$previousPaneSizeStore && $previousPaneSizeStore.maxSize < 100
? $previousPaneSizeStore.maxSize
: undefined}
/>
{/if}

<!-- Pane -->
Expand Down
57 changes: 41 additions & 16 deletions src/lib/Splitpanes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

<script lang="ts">
import { onMount, onDestroy, setContext, createEventDispatcher, tick } from 'svelte';
import { writable } from 'svelte/store';
import type { IPane, IPaneSizingEvent, SplitContext, PaneInitFunction } from '.';
import { writable, type Readable } from 'svelte/store';
import type { SizeDetails, IPane, IPaneSizingEvent, SplitContext, PaneInitFunction } from '.';
import { switchableStore } from './internal/store';

// TYPE DECLARATIONS ----------------

Expand Down Expand Up @@ -99,7 +100,11 @@
let isHorizontal = writable<boolean>(horizontal);
const showFirstSplitter = writable<boolean>(firstSplitter);
// tells the key of the very first pane, or undefined if not recieved yet
const veryFirstPaneKey = writable<any>(undefined);
let lastPreviousPanesSizeStoreOnInit: Readable<SizeDetails> | undefined = undefined;
const previousPanesSizeStoresData = new Map<
any,
{ receivedStore: Readable<SizeDetails>; update: (store: Readable<SizeDetails> | undefined) => void }
>();
let activeSplitterElement: HTMLElement | null = null;
let activeSplitterDrag: number | null = null;
let startingTDrag: number | null = null;
Expand All @@ -115,10 +120,11 @@
});
}

const onPaneInit: PaneInitFunction = (key: any) => {
if ($veryFirstPaneKey === undefined) {
$veryFirstPaneKey = key;
}
const onPaneInit: PaneInitFunction = (key: any, sizeStore: Readable<SizeDetails>) => {
const { store, update } = switchableStore(lastPreviousPanesSizeStoreOnInit, undefined);
lastPreviousPanesSizeStoreOnInit = sizeStore;

previousPanesSizeStoresData.set(key, { receivedStore: sizeStore, update });

return {
onSplitterDown: (e) => {
Expand All @@ -137,13 +143,13 @@
if (dblClickSplitter) {
onSplitterDblClick(e, indexOfPane(key));
}
}
},
previousPaneSizeStore: store
};
};

setContext<SplitContext>(KEY, {
showFirstSplitter,
veryFirstPaneKey,
isHorizontal,
onPaneInit,
onPaneAdd,
Expand All @@ -159,11 +165,6 @@
return el === pane.element;
});

if (index === 0) {
// Need to update the first pane key, because the first pane can be changed in runtime.
$veryFirstPaneKey = pane.key;
}

//inserts pane at proper array index
panes.splice(index, 0, pane);

Expand All @@ -172,6 +173,20 @@
panes[i].index = i;
}

// Update previous size stores both both this pane and for the next one
const currentData = previousPanesSizeStoresData.get(pane.key);
const previousSizeStoreNow =
index === 0 ? undefined : previousPanesSizeStoresData.get(panes[index - 1].key).receivedStore;
currentData.update(previousSizeStoreNow);

if (index < panes.length - 1) {
// If isn't not the last one
const currentSizeStore = currentData.receivedStore;

const nextKey = panes[index + 1].key;
previousPanesSizeStoresData.get(nextKey).update(currentSizeStore);
}

if (isReady) {
await tick();

Expand All @@ -189,6 +204,7 @@
async function onPaneRemove(key: any) {
// 1. Remove the pane from array and redo indexes.
const index = panes.findIndex((p) => p.key === key);
const isNotLast = index < panes.length - 1;

// race condition - typically happens when the dev server restarts
if (index >= 0) {
Expand All @@ -199,8 +215,17 @@
panes[i].index = i;
}

if (index === 0) {
$veryFirstPaneKey = panes.length > 0 ? panes[0].key : undefined;
// Update previous size stores both both this pane and for the next one
previousPanesSizeStoresData.get(key).update(undefined);

if (isNotLast) {
// If not the last one
const prevIndex = index - 1;
const prevSizeStoreNow =
prevIndex === 0 ? undefined : previousPanesSizeStoresData.get(panes[prevIndex].key).receivedStore;

const nextKey = panes[index].key;
previousPanesSizeStoresData.get(nextKey).update(prevSizeStoreNow);
}

if (isReady) {
Expand Down
24 changes: 21 additions & 3 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,34 @@ import type { Readable } from 'svelte/store';
export { default as Splitpanes } from './Splitpanes.svelte';
export { default as Pane } from './Pane.svelte';

export type PaneInitFunction = (key: any) => {
export interface SizeDetails {
/** The current size of a pane.
*
* If number, it's the real pane size value.
* If null, it means that the pane size is unknown yet.
*/
size: number | null;
minSize: number;
maxSize: number;
}

export type PaneInitFunction = (
key: any,
sizeStore: Readable<SizeDetails>
) => {
onSplitterDown: (_event: TouchEvent | MouseEvent) => void;
onSplitterClick: (event: MouseEvent) => void;
onSplitterDblClick: (_event: MouseEvent) => void;
/** A store that tells what is the previous pane size.
*
* If undefined, it means that you are the first pane.
* Otherwise, it contains the details about the previous panel size details.
*/
previousPaneSizeStore: Readable<SizeDetails | undefined>;
};

// methods passed from splitpane to children panes
export interface SplitContext {
/** Tells the key of the very first pane, or undefined if not recieved yet. */
veryFirstPaneKey: Readable<any>;
isHorizontal: Readable<boolean>;
showFirstSplitter: Readable<boolean>;
onPaneInit: PaneInitFunction;
Expand Down
51 changes: 51 additions & 0 deletions src/lib/internal/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { readable, type Readable } from 'svelte/store';

export interface SwitchableStoreResult<T> {
store: Readable<T>;
update: (store?: Readable<T>) => void;
}

export function switchableStore<T extends K, K>(
initialStore?: Readable<T> | undefined,
unsetValue: K = undefined
): SwitchableStoreResult<T> {
let currentStore = initialStore;
let _set: ((value: T) => void) | undefined = undefined;
let unsubscribe: (() => void) | undefined = undefined;

function makeSubscription(store: Readable<T> | undefined, set: (value: T) => void) {
if (store) {
return store.subscribe((val) => {
set(val);
});
} else {
set(unsetValue as T);
return undefined;
}
}

const store = readable<T>(undefined as T, (set) => {
unsubscribe = makeSubscription(currentStore, set);
_set = set;

return () => {
if (unsubscribe) {
unsubscribe();
}
unsubscribe = undefined;
_set = undefined;
};
});

const update = (store?: Readable<T>) => {
currentStore = store;
if (_set) {
if (unsubscribe) {
unsubscribe();
}
unsubscribe = makeSubscription(currentStore, _set);
}
};

return { store, update };
}