Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useHighContrastMode } from '../../composables/use-high-contrast-mode';
import { toolbarIcons } from './toolbarIcons.js';

const { isHighContrastMode } = useHighContrastMode();
const emit = defineEmits(['select']);

const props = defineProps({
selectedStyle: {
type: String,
default: null,
},
});

const buttonRefs = ref([]);
const bulletButtons = [
{ key: 'disc', icon: toolbarIcons.bulletListDisc, ariaLabel: 'Opaque circle' },
{ key: 'circle', icon: toolbarIcons.bulletListCircle, ariaLabel: 'Outline circle' },
{ key: 'square', icon: toolbarIcons.bulletListSquare, ariaLabel: 'Opaque square' },
];

const select = (key) => {
emit('select', key);
};

const moveToNextButton = (index) => {
if (index === buttonRefs.value.length - 1) return;
const next = buttonRefs.value[index + 1];
if (next) {
next.setAttribute('tabindex', '0');
next.focus();
}
};

const moveToPreviousButton = (index) => {
if (index === 0) return;
const prev = buttonRefs.value[index - 1];
if (prev) {
prev.setAttribute('tabindex', '0');
prev.focus();
}
};

const handleKeyDown = (e, index) => {
switch (e.key) {
case 'ArrowLeft':
moveToPreviousButton(index);
break;
case 'ArrowRight':
moveToNextButton(index);
break;
case 'Enter':
select(bulletButtons[index].key);
break;
default:
break;
}
};

onMounted(() => {
const first = buttonRefs.value[0];
if (first) {
first.setAttribute('tabindex', '0');
first.focus();
}
});
</script>

<template>
<div class="bullet-style-buttons" :class="{ 'high-contrast': isHighContrastMode }">
<div
v-for="(button, index) in bulletButtons"
:key="button.key"
class="button-icon"
:class="{ selected: props.selectedStyle === button.key }"
@click="select(button.key)"
v-html="button.icon"
role="menuitem"
:aria-label="button.ariaLabel"
ref="buttonRefs"
@keydown.prevent="(event) => handleKeyDown(event, index)"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow Tab to leave list-style dropdown options

Using @keydown.prevent on each style button prevents default behavior for all keys, but the handler only processes arrow keys and Enter. When a user tabs onto these options, pressing Tab is suppressed, so keyboard users cannot advance focus out of the dropdown via normal tab navigation (same pattern is used in the numbered variant), creating an accessibility and usability regression.

Useful? React with 👍 / 👎.

></div>
</div>
</template>

<style scoped>
.bullet-style-buttons {
display: flex;
justify-content: space-between;
width: 100%;
padding: 8px;
box-sizing: border-box;

.button-icon {
cursor: pointer;
padding: 5px;
font-size: var(--sd-ui-font-size-600, 16px);
color: var(--sd-ui-dropdown-text, #47484a);
width: 25px;
height: 25px;
border-radius: var(--sd-ui-dropdown-option-radius, 3px);
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;

&:hover {
background-color: var(--sd-ui-dropdown-hover-bg, #d8dee5);
color: var(--sd-ui-dropdown-hover-text, #47484a);
}

:deep(svg) {
width: 100%;
height: 100%;
display: block;
fill: currentColor;
}

&.selected {
background-color: var(--sd-ui-dropdown-active-bg, #d8dee5);
color: var(--sd-ui-dropdown-selected-text, #47484a);
}
}

&.high-contrast {
.button-icon {
&:hover,
&.selected {
background-color: #000;
color: #fff;
}
}
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useHighContrastMode } from '../../composables/use-high-contrast-mode';
import { toolbarIcons } from './toolbarIcons.js';

const { isHighContrastMode } = useHighContrastMode();
const emit = defineEmits(['select']);

const props = defineProps({
selectedStyle: {
type: String,
default: null,
},
});

const buttonRefs = ref([]);
const numberedButtons = [
{ key: 'decimal', icon: toolbarIcons.numberedListDecimal, ariaLabel: '1. 2. 3.' },
{ key: 'decimal-paren', icon: toolbarIcons.numberedListDecimalParen, ariaLabel: '1) 2) 3)' },
{ key: 'upper-roman', icon: toolbarIcons.numberedListUpperRoman, ariaLabel: 'I. II. III.' },
{ key: 'lower-roman', icon: toolbarIcons.numberedListLowerRoman, ariaLabel: 'i. ii. iii.' },
{ key: 'upper-alpha', icon: toolbarIcons.numberedListUpperAlpha, ariaLabel: 'A. B. C.' },
{ key: 'lower-alpha', icon: toolbarIcons.numberedListLowerAlpha, ariaLabel: 'a. b. c.' },
{ key: 'lower-alpha-paren', icon: toolbarIcons.numberedListLowerAlphaParen, ariaLabel: 'a) b) c)' },
];

const select = (key) => {
emit('select', key);
};

const moveToNextButton = (index) => {
if (index === buttonRefs.value.length - 1) return;
const next = buttonRefs.value[index + 1];
if (next) {
next.setAttribute('tabindex', '0');
next.focus();
}
};

const moveToPreviousButton = (index) => {
if (index === 0) return;
const prev = buttonRefs.value[index - 1];
if (prev) {
prev.setAttribute('tabindex', '0');
prev.focus();
}
};

const handleKeyDown = (e, index) => {
switch (e.key) {
case 'ArrowLeft':
moveToPreviousButton(index);
break;
case 'ArrowRight':
moveToNextButton(index);
break;
case 'Enter':
select(numberedButtons[index].key);
break;
default:
break;
}
};

onMounted(() => {
const first = buttonRefs.value[0];
if (first) {
first.setAttribute('tabindex', '0');
first.focus();
}
});
</script>

<template>
<div class="numbered-style-buttons" :class="{ 'high-contrast': isHighContrastMode }">
<div
v-for="(button, index) in numberedButtons"
:key="button.key"
class="button-icon"
:class="{ selected: props.selectedStyle === button.key }"
@click="select(button.key)"
v-html="button.icon"
role="menuitem"
:aria-label="button.ariaLabel"
ref="buttonRefs"
@keydown.prevent="(event) => handleKeyDown(event, index)"
></div>
</div>
</template>

<style scoped>
.numbered-style-buttons {
display: flex;
justify-content: space-between;
width: 100%;
padding: 8px;
box-sizing: border-box;

.button-icon {
cursor: pointer;
padding: 5px;
font-size: var(--sd-ui-font-size-600, 16px);
color: var(--sd-ui-dropdown-text, #47484a);
width: 30px;
height: 30px;
border-radius: var(--sd-ui-dropdown-option-radius, 3px);
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;

&:hover {
background-color: var(--sd-ui-dropdown-hover-bg, #d8dee5);
color: var(--sd-ui-dropdown-hover-text, #47484a);
}

:deep(svg) {
width: 100%;
height: 100%;
display: block;
fill: currentColor;
}

&.selected {
background-color: var(--sd-ui-dropdown-active-bg, #d8dee5);
color: var(--sd-ui-dropdown-selected-text, #47484a);
}
}

&.high-contrast {
.button-icon {
&:hover,
&.selected {
background-color: #000;
color: #fff;
}
}
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { sanitizeNumber } from './helpers';
import { useToolbarItem } from './use-toolbar-item';
import AIWriter from './AIWriter.vue';
import AlignmentButtons from './AlignmentButtons.vue';
import BulletStyleButtons from './BulletStyleButtons.vue';
import NumberedStyleButtons from './NumberedStyleButtons.vue';
import DocumentMode from './DocumentMode.vue';
import LinkedStyle from './LinkedStyle.vue';
import LinkInput from './LinkInput.vue';
Expand Down Expand Up @@ -630,30 +632,66 @@ export const makeDefaultItems = ({

// bullet list
const bulletedList = useToolbarItem({
type: 'button',
type: 'dropdown',
name: 'list',
command: 'toggleBulletList',
command: 'toggleBulletListStyle',
icon: toolbarIcons.bulletList,
active: false,
hasCaret: true,
tooltip: toolbarTexts.bulletList,
restoreEditorFocus: true,
suppressActiveHighlight: true,
attributes: {
ariaLabel: 'Bullet list',
},
options: [
{
type: 'render',
key: 'bullet-style-buttons',
render: () => {
const handleSelect = (style) => {
closeDropdown(bulletedList);
const item = { ...bulletedList, command: 'toggleBulletListStyle' };
superToolbar.emitCommand({ item, argument: style });
};
return h(BulletStyleButtons, {
selectedStyle: bulletedList.selectedValue.value,
onSelect: handleSelect,
});
},
},
],
});

// number list
const numberedList = useToolbarItem({
type: 'button',
type: 'dropdown',
name: 'numberedlist',
command: 'toggleOrderedList',
command: 'toggleOrderedListStyle',
icon: toolbarIcons.numberedList,
active: false,
hasCaret: true,
tooltip: toolbarTexts.numberedList,
restoreEditorFocus: true,
suppressActiveHighlight: true,
attributes: {
ariaLabel: 'Numbered list',
},
options: [
{
type: 'render',
key: 'numbered-style-buttons',
render: () => {
const handleSelect = (style) => {
closeDropdown(numberedList);
const item = { ...numberedList, command: 'toggleOrderedListStyle' };
superToolbar.emitCommand({ item, argument: style });
};
return h(NumberedStyleButtons, {
selectedStyle: numberedList.selectedValue.value,
onSelect: handleSelect,
});
},
},
],
});

// indent left
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useToolbarItem } from '@components/toolbar/use-toolbar-item';
import { calculateResolvedParagraphProperties } from '@extensions/paragraph/resolvedPropertiesCache.js';
import { parseSizeUnit } from '@core/utilities';
import { findElementBySelector, getParagraphFontFamilyFromProperties } from './helpers/general.js';
import { markerTextToBulletStyle } from '@helpers/list-numbering-helpers.js';

/**
* @typedef {function(CommandItem): void} CommandCallback
Expand Down Expand Up @@ -622,6 +623,24 @@ export class SuperToolbar extends EventEmitter {
if (commandState?.value != null) item.activate({ styleId: commandState.value });
else item.label.value = this.config.texts?.formatText || 'Format text';
},
list: () => {
if (commandState?.active) {
item.activate();
item.selectedValue.value = markerTextToBulletStyle(commandState.value);
} else {
item.deactivate();
item.selectedValue.value = null;
}
},
numberedlist: () => {
if (commandState?.active) {
item.activate();
item.selectedValue.value = commandState.value;
} else {
item.deactivate();
item.selectedValue.value = null;
}
},
default: () => {
if (commandState?.active) item.activate();
else item.deactivate();
Expand Down
Loading
Loading