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.

P1 Badge Allow Tab key to escape bullet style menu

The @keydown.prevent modifier on each bullet-style option prevents default behavior for all keys, not just the handled arrows/Enter. In practice, once focus is inside this menu, pressing Tab cannot move focus to the next control, which creates a keyboard trap for keyboard-only users when the dropdown is open. This should only prevent default for keys you explicitly handle.

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
Expand Up @@ -4,6 +4,7 @@ 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 DocumentMode from './DocumentMode.vue';
import LinkedStyle from './LinkedStyle.vue';
import LinkInput from './LinkInput.vue';
Expand Down Expand Up @@ -630,16 +631,34 @@ 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
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,15 @@ 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;
}
},
default: () => {
if (commandState?.active) item.activate();
else item.deactivate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import boldIconSvg from '@superdoc/common/icons/bold-solid.svg?raw';
import italicIconSvg from '@superdoc/common/icons/italic-solid.svg?raw';
import underlineIconSvg from '@superdoc/common/icons/underline-solid.svg?raw';
import listIconSvg from '@superdoc/common/icons/list-solid.svg?raw';
import listCircleIconSvg from '@superdoc/common/icons/list-circle-solid.svg?raw';
import listSquareIconSvg from '@superdoc/common/icons/list-square-solid.svg?raw';
import listOlIconSvg from '@superdoc/common/icons/list-ol-solid.svg?raw';
import imageIconSvg from '@superdoc/common/icons/image-solid.svg?raw';
import linkIconSvg from '@superdoc/common/icons/link-solid.svg?raw';
Expand Down Expand Up @@ -64,6 +66,9 @@ export const toolbarIcons = {
alignCenter: alignCenterIconSvg,
alignJustify: alignJustifyIconSvg,
bulletList: listIconSvg,
bulletListDisc: listIconSvg,
bulletListCircle: listCircleIconSvg,
bulletListSquare: listSquareIconSvg,
numberedList: listOlIconSvg,
indentLeft: outdentIconSvg,
indentRight: indentIconSvg,
Expand Down
17 changes: 11 additions & 6 deletions packages/super-editor/src/editors/v1/core/commands/toggleList.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @ts-check
import { updateNumberingProperties } from './changeListLevel.js';
import { ListHelpers } from '@helpers/list-numbering-helpers.js';
import { ListHelpers, markerTextToBulletStyle } from '@helpers/list-numbering-helpers.js';
import { getResolvedParagraphProperties } from '@extensions/paragraph/resolvedPropertiesCache.js';
import { isVisuallyEmptyParagraph } from './removeNumberingProperties.js';
import { Selection, TextSelection } from 'prosemirror-state';
Expand All @@ -26,10 +26,15 @@ function getParagraphListKind(node, editor) {
return numFmtIsBullet(fmt) ? 'bullet' : 'ordered';
}

function paragraphMatchesToggleListType(node, editor, listType) {
function paragraphMatchesToggleListType(node, editor, listType, bulletStyle) {
const kind = getParagraphListKind(node, editor);
if (!kind) return false;
if (listType === 'bulletList') return kind === 'bullet';
if (listType === 'bulletList') {
if (kind !== 'bullet') return false;
if (!bulletStyle) return true;
const markerText = node.attrs.listRendering?.markerText;
return markerTextToBulletStyle(markerText) === bulletStyle;
}
if (listType === 'orderedList') return kind === 'ordered';
return false;
}
Expand Down Expand Up @@ -60,13 +65,13 @@ function getPrecedingParagraphForListReuse(doc, from, paragraphsInSelection) {
}

export const toggleList =
(listType) =>
(listType, bulletStyle) =>
({ editor, state, tr, dispatch }) => {
if (listType !== 'orderedList' && listType !== 'bulletList') {
return false;
}

const predicate = (n) => paragraphMatchesToggleListType(n, editor, listType);
const predicate = (n) => paragraphMatchesToggleListType(n, editor, listType, bulletStyle);
const { selection } = state;
const { from, to } = selection;
let firstListNode = null;
Expand Down Expand Up @@ -127,7 +132,7 @@ export const toggleList =

if (mode === 'create') {
const numId = ListHelpers.getNewListId(editor);
ListHelpers.generateNewListDefinition({ numId: Number(numId), listType, editor });
ListHelpers.generateNewListDefinition({ numId: Number(numId), listType, editor, bulletStyle });
sharedNumberingProperties = {
numId: Number(numId),
ilvl: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ import { mutateNumbering } from '@core/parts/adapters/numbering-mutation';
// Shims will be removed as callers migrate in Phases 1b–1d.
// ---------------------------------------------------------------------------

/**
* Maps a bullet marker character (from `listRendering.markerText`) to its named bullet style.
* Returns null for unrecognized markers.
* @param {string|null|undefined} markerText
* @returns {'disc'|'circle'|'square'|null}
*/
export function markerTextToBulletStyle(markerText) {
const map = { '•': 'disc', '◦': 'circle', '▪': 'square' };
return map[markerText] ?? null;
}

/**
* Generate a new list definition for the given list type.
* @param {Object} param0
Expand All @@ -39,10 +50,21 @@ import { mutateNumbering } from '@core/parts/adapters/numbering-mutation';
* @param {string} [param0.text]
* @param {string} [param0.fmt]
* @param {string} [param0.markerFontFamily]
* @param {'disc'|'circle'|'square'} [param0.bulletStyle]
* @param {import('../Editor').Editor} param0.editor
* @returns {Object} The new abstract and num definitions.
*/
export const generateNewListDefinition = ({ numId, listType, level, start, text, fmt, editor, markerFontFamily }) => {
export const generateNewListDefinition = ({
numId,
listType,
level,
start,
text,
fmt,
editor,
markerFontFamily,
bulletStyle,
}) => {
/** @type {{ abstractDef: any, numDef: any }} */
let resultDefs;

Expand All @@ -55,6 +77,7 @@ export const generateNewListDefinition = ({ numId, listType, level, start, text,
text,
fmt,
markerFontFamily,
bulletStyle,
});
resultDefs = { abstractDef: result.abstractDef, numDef: result.numDef };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,15 @@ interface GenerateOptions {
text?: string | null;
fmt?: string | null;
markerFontFamily?: string | null;
bulletStyle?: 'disc' | 'circle' | 'square' | null;
}

const BULLET_STYLE_CHARS: Record<string, string> = {
disc: '•',
circle: '◦',
square: '▪',
};

interface GenerateResult {
numId: number;
abstractId: number;
Expand Down Expand Up @@ -72,7 +79,7 @@ function buildNumDef(numId: number, abstractId: number): any {
*/
export function generateNewListDefinition(numbering: NumberingModel, options: GenerateOptions): GenerateResult {
let { listType } = options;
const { numId, level, start, text, fmt, markerFontFamily } = options;
const { numId, level, start, text, fmt, markerFontFamily, bulletStyle } = options;
if (typeof listType !== 'string') listType = (listType as any).name;

const definition = listType === 'orderedList' ? baseOrderedListDef : baseBulletList;
Expand All @@ -86,6 +93,25 @@ export function generateNewListDefinition(numbering: NumberingModel, options: Ge
}),
);

// Override the bullet style for the new list if a bullet style is provided
const shouldOverrideBulletStyle = bulletStyle && listType !== 'orderedList';
if (shouldOverrideBulletStyle) {
const char = BULLET_STYLE_CHARS[bulletStyle];

if (char) {
const lvl0 = newAbstractDef.elements.find((el: any) => el.name === 'w:lvl' && el.attributes['w:ilvl'] === '0');

if (lvl0) {
const lvlText = lvl0.elements.find((el: any) => el.name === 'w:lvlText');
if (lvlText) lvlText.attributes['w:val'] = char;

// Remove any inherited font so the Unicode char renders in the document's default font
const rPr = lvl0.elements.find((el: any) => el.name === 'w:rPr');
if (rPr) rPr.elements = rPr.elements.filter((el: any) => el.name !== 'w:rFonts');
}
}
}

if (level != null && start != null && text != null && fmt != null) {
if (numbering.definitions[numId]) {
const abstractId = numbering.definitions[numId]?.elements[0]?.attributes['w:val'];
Expand Down
Loading
Loading