Skip to content

Commit

Permalink
Improve editor accessibility (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
BearToCode authored Dec 17, 2023
2 parents 1b75fe7 + 4e097b8 commit b7c8704
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 30 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
- Toolbar (extensible);
- Markdown syntax highlighting;
- Scroll sync;
- Accessibility friendly;
- **SSR** compatible;
- **Katex** support (plugin);
- **Slash** commands (plugin);
- **Emojis**, with included search (plugin);
- **Tikz** support(plugin);
- **Attachment** support(plugin);
- **Tikz** support (plugin);
- **Attachment** support (plugin);
- Code blocks **syntax highlighting** (plugin).

## Packages
Expand Down
3 changes: 2 additions & 1 deletion docs/src/pages/introduction.svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ section: Overview
Carta is a lightweight, fast and extensible Svelte Markdown editor and viewer, designed for flexibility. It works natively in SvelteKit, and supports Server Side Rendering.

## Core Features
## Features

- **Lightweight**: no code editor is included, just a textarea with syntax highlighting, with Markdown related utilities.
- **SSR compatible**: works great with SvelteKit.
- **Keyboard shortcuts**: extensible and configurable.
- **Toolbar**: add or remove buttons according to your needs.
- **Plugins friendly**: easily create your own extension.
- **Accessibility**: includes ARIA roles, arrow keys navigation and labels.

## Official Plugins

Expand Down
18 changes: 13 additions & 5 deletions packages/carta-md/src/lib/CartaEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { debounce } from './internal/utils';
import type { TextAreaProps } from './internal/textarea-props';
import { DefaultCartaLabels, type CartaLabels } from './internal/labels';
import { handleArrowKeysNavigation } from './internal/accessibility';
export let carta: Carta;
export let theme = 'default';
Expand Down Expand Up @@ -99,35 +100,42 @@

<div bind:this={editorElem} bind:clientWidth={width} class="carta-editor carta-theme__{theme}">
{#if !disableToolbar}
<div class="carta-toolbar">
<div class="carta-toolbar" role="toolbar">
<div class="carta-toolbar-left">
{#if windowMode == 'tabs'}
<button
type="button"
on:click={() => (selectedTab = 'write')}
tabindex={0}
class={selectedTab === 'write' ? 'carta-active' : ''}
on:click={() => (selectedTab = 'write')}
on:keydown={handleArrowKeysNavigation}
>
{labels.writeTab}
</button>
<button
type="button"
on:click={() => (selectedTab = 'preview')}
tabindex={-1}
class={selectedTab === 'preview' ? 'carta-active' : ''}
on:click={() => (selectedTab = 'preview')}
on:keydown={handleArrowKeysNavigation}
>
{labels.previewTab}
</button>
{/if}
</div>
<div class="carta-toolbar-right">
{#if !hideIcons}
{#each carta.icons as icon}
{#each carta.icons as icon, index}
<button
class="carta-icon"
tabindex={index == 0 ? 0 : -1}
aria-label={icon.label}
on:click|preventDefault|stopPropagation={() => {
carta.input && icon.action(carta.input);
carta.input?.update();
carta.input?.textarea.focus();
}}
class="carta-icon"
on:keydown={handleArrowKeysNavigation}
>
<svelte:component this={icon.component} />
</button>
Expand Down
21 changes: 21 additions & 0 deletions packages/carta-md/src/lib/internal/accessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Handles arrow key navigation for a list of elements.
* @param e The event to handle.
*/
export function handleArrowKeysNavigation(e: KeyboardEvent & { currentTarget: HTMLElement }) {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
e.preventDefault();

const siblings = e.currentTarget.parentElement?.children;
if (!siblings) return;

const next = (e.currentTarget.nextElementSibling ?? siblings[0]) as HTMLElement;
const prev = (e.currentTarget.previousElementSibling ??
siblings[siblings.length - 1]) as HTMLElement;

if (e.key === 'ArrowRight') {
next.focus();
} else {
prev.focus();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,33 @@
onMount(setInput);
</script>

<div role="tooltip" id="editor-unfocus-suggestion">
Press ESC then TAB to move the focus off the field
</div>
<div
on:click={focus}
on:keydown={focus}
on:scroll={handleScroll}
role="textbox"
tabindex="0"
tabindex="-1"
class="carta-input"
bind:this={elem}
>
<div class="carta-input-wrapper">
<pre
class="shj-lang-md carta-font-code"
bind:this={highlighElem}
tabindex="-1"
aria-hidden="true"><!-- eslint-disable-line svelte/no-at-html-tags -->{@html highlighted}</pre>

<textarea
name="md"
id="md"
spellcheck="false"
class="carta-font-code"
aria-multiline="true"
aria-describedby="editor-unfocus-suggestion"
tabindex="0"
{placeholder}
{...props}
bind:value
Expand Down Expand Up @@ -125,4 +132,15 @@
white-space: pre-wrap;
word-break: break-word;
}
#editor-unfocus-suggestion {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
</style>
44 changes: 34 additions & 10 deletions packages/carta-md/src/lib/internal/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,61 @@ import StrikethroughIcon from './components/icons/StrikethroughIcon.svelte';
* Editor toolbar icon information.
*/
export interface CartaIcon {
/**
* The icon's unique identifier.
*/
id: string;
/**
* Callback function to execute when the icon is clicked.
* @param input CartaInput instance
*/
action: (input: CartaInput) => void;
/**
* The icon's component.
*/
component: ComponentType;
/**
* The icon's label (used as aria-label).
*/
label?: string;
}

export const defaultIcons = [
{
id: 'heading',
action: (input) => input.toggleLinePrefix('###'),
component: HeadingIcon
component: HeadingIcon,
label: 'Heading'
},
{
id: 'bold',
action: (input) => input.toggleSelectionSurrounding('**'),
component: BoldIcon
component: BoldIcon,
label: 'Bold'
},
{
id: 'italic',
action: (input) => input.toggleSelectionSurrounding('_'),
component: ItalicIcon
component: ItalicIcon,
label: 'Italic'
},
{
id: 'strikethrough',
action: (input) => input.toggleSelectionSurrounding('~~'),
component: StrikethroughIcon
component: StrikethroughIcon,
label: 'Strikethrough'
},
{
id: 'quote',
action: (input) => input.toggleLinePrefix('>'),
component: QuoteIcon
component: QuoteIcon,
label: 'Quote'
},
{
id: 'code',
action: (input) => input.toggleSelectionSurrounding('`'),
component: CodeIcon
component: CodeIcon,
label: 'Code'
},
{
id: 'link',
Expand All @@ -59,22 +79,26 @@ export const defaultIcons = [
input.insertAt(position, '(url)');
input.textarea.setSelectionRange(position + 1, position + 4);
},
component: LinkIcon
component: LinkIcon,
label: 'Link'
},
{
id: 'bulletedList',
action: (input) => input.toggleLinePrefix('- ', 'detach'),
component: ListBulletedIcon
component: ListBulletedIcon,
label: 'Bulleted list'
},
{
id: 'numberedList',
action: (input) => input.toggleLinePrefix('1. ', 'detach'),
component: ListNumberedIcon
component: ListNumberedIcon,
label: 'Numbered list'
},
{
id: 'taskList',
action: (input) => input.toggleLinePrefix('- [ ] ', 'detach'),
component: ListTaskIcon
component: ListTaskIcon,
label: 'Task list'
}
] as const satisfies readonly CartaIcon[];

Expand Down
35 changes: 27 additions & 8 deletions packages/carta-md/src/lib/internal/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface InputSettings {

export class CartaInput {
private pressedKeys: Set<string>;
private escapePressed = false;
// Used to detect keys that actually changed the textarea value
private onKeyDownValue: string | undefined;

Expand All @@ -45,6 +46,7 @@ export class CartaInput {

textarea.addEventListener('focus', () => {
this.pressedKeys.clear();
this.escapePressed = false;
});
textarea.addEventListener('blur', () => {
this.pressedKeys.clear();
Expand Down Expand Up @@ -123,13 +125,33 @@ export class CartaInput {
if (key === 'enter') {
// Check prefixes
this.handleNewLine(e);
} else if (key == 'tab') {
} else if (key == 'tab' && !this.escapePressed) {
e.preventDefault(); // Don't select other stuff
const position = this.textarea.selectionStart;
this.insertAt(this.textarea.selectionStart, '\t');
this.textarea.selectionStart = position + 1;
this.textarea.selectionEnd = position + 1;

if (e.shiftKey) {
// Unindent
const line = this.getLine();
const lineStart = line.start;
const lineContent = line.value;
const position = this.textarea.selectionStart;

// Check if the line starts with a tab
if (lineContent.startsWith('\t')) {
// Remove the tab
this.removeAt(lineStart, 1);
this.textarea.selectionStart = position - 1;
this.textarea.selectionEnd = position - 1;
}
} else {
const position = this.textarea.selectionStart;
this.insertAt(this.textarea.selectionStart, '\t');
this.textarea.selectionStart = position + 1;
this.textarea.selectionEnd = position + 1;
}

this.update();
} else if (key === 'escape') {
this.escapePressed = true;
}
this.onKeyDownValue = this.textarea.value;
}
Expand Down Expand Up @@ -219,9 +241,6 @@ export class CartaInput {
start: lineStartingIndex,
end: lineEndingIndex,
value: this.textarea.value.slice(lineStartingIndex, lineEndingIndex)
/**
* Position of the cursor relative to the line.
*/
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/carta-md/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</svelte:head>

<main>
<CartaEditor placeholder="Some text..." mode="split" {carta} />
<CartaEditor placeholder="Some text..." mode="tabs" {carta} />
</main>

<style>
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-attachment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"!dist/**/*.spec.*"
],
"peerDependencies": {
"carta-md": "^3.0.0",
"carta-md": "^3.4.0",
"marked": "^9.1.5",
"svelte": "^3.54.0 || ^4.0.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-attachment/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ export const attachment = (options: AttachmentExtensionOptions): CartaExtension

input.click();
},
id: 'attach'
id: 'attach',
label: 'Attach file'
}
]
};
Expand Down

0 comments on commit b7c8704

Please sign in to comment.