diff --git a/blocks/logic.ts b/blocks/logic.ts index d2a7405fffa..008bd8c6c15 100644 --- a/blocks/logic.ts +++ b/blocks/logic.ts @@ -35,6 +35,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'BOOL', + 'ariaTypeName': 'Boolean', 'options': [ ['%{BKY_LOGIC_BOOLEAN_TRUE}', 'TRUE'], ['%{BKY_LOGIC_BOOLEAN_FALSE}', 'FALSE'], @@ -117,13 +118,14 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', + 'ariaTypeName': 'Operator', 'options': [ - ['=', 'EQ'], - ['\u2260', 'NEQ'], - ['\u200F<', 'LT'], - ['\u200F\u2264', 'LTE'], - ['\u200F>', 'GT'], - ['\u200F\u2265', 'GTE'], + ['=', 'EQ', 'Equals'], + ['\u2260', 'NEQ', 'Does not equal'], + ['\u200F<', 'LT', 'Less than'], + ['\u200F\u2264', 'LTE', 'Less than or equal to'], + ['\u200F>', 'GT', 'Greater than'], + ['\u200F\u2265', 'GTE', 'Greater than or equal to'], ], }, { @@ -150,9 +152,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', + 'ariaTypeName': 'Boolean operator', 'options': [ - ['%{BKY_LOGIC_OPERATION_AND}', 'AND'], - ['%{BKY_LOGIC_OPERATION_OR}', 'OR'], + ['%{BKY_LOGIC_OPERATION_AND}', 'AND', 'And'], + ['%{BKY_LOGIC_OPERATION_OR}', 'OR', 'Or'], ], }, { diff --git a/blocks/loops.ts b/blocks/loops.ts index 6d450e53215..5055383a565 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -92,9 +92,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'MODE', + 'ariaTypeName': 'Repeat type', 'options': [ - ['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}', 'WHILE'], - ['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}', 'UNTIL'], + ['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}', 'WHILE', 'While'], + ['%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}', 'UNTIL', 'Until'], ], }, { @@ -199,9 +200,18 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'FLOW', + 'ariaTypeName': 'Continue type', 'options': [ - ['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', 'BREAK'], - ['%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', 'CONTINUE'], + [ + '%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}', + 'BREAK', + 'Break loop', + ], + [ + '%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}', + 'CONTINUE', + 'Continue loop', + ], ], }, ], diff --git a/blocks/math.ts b/blocks/math.ts index b756967832e..08d7ff9b4c6 100644 --- a/blocks/math.ts +++ b/blocks/math.ts @@ -32,7 +32,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'type': 'field_number', 'name': 'NUM', 'value': 0, - 'ariaName': 'Number', + 'ariaTypeName': 'Number', }, ], 'output': 'Number', @@ -55,7 +55,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', - 'ariaName': 'Arithmetic operation', + 'ariaTypeName': 'Operator', 'options': [ ['%{BKY_MATH_ADDITION_SYMBOL}', 'ADD', 'Plus'], ['%{BKY_MATH_SUBTRACTION_SYMBOL}', 'MINUS', 'Minus'], @@ -85,14 +85,15 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', + 'ariaTypeName': 'Function', 'options': [ - ['%{BKY_MATH_SINGLE_OP_ROOT}', 'ROOT'], - ['%{BKY_MATH_SINGLE_OP_ABSOLUTE}', 'ABS'], - ['-', 'NEG'], - ['ln', 'LN'], - ['log10', 'LOG10'], - ['e^', 'EXP'], - ['10^', 'POW10'], + ['%{BKY_MATH_SINGLE_OP_ROOT}', 'ROOT', 'Square root of '], + ['%{BKY_MATH_SINGLE_OP_ABSOLUTE}', 'ABS', 'Absolute value of'], + ['-', 'NEG', 'Negative value of'], + ['ln', 'LN', 'Natural logarithm of'], + ['log10', 'LOG10', 'Logarithm base 10 of'], + ['e^', 'EXP', 'e to the power of'], + ['10^', 'POW10', '10 to the power of'], ], }, { @@ -115,13 +116,14 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', + 'ariaTypeName': 'Trigonometry function', 'options': [ - ['%{BKY_MATH_TRIG_SIN}', 'SIN'], - ['%{BKY_MATH_TRIG_COS}', 'COS'], - ['%{BKY_MATH_TRIG_TAN}', 'TAN'], - ['%{BKY_MATH_TRIG_ASIN}', 'ASIN'], - ['%{BKY_MATH_TRIG_ACOS}', 'ACOS'], - ['%{BKY_MATH_TRIG_ATAN}', 'ATAN'], + ['%{BKY_MATH_TRIG_SIN}', 'SIN', 'Sine of'], + ['%{BKY_MATH_TRIG_COS}', 'COS', 'Cosine of'], + ['%{BKY_MATH_TRIG_TAN}', 'TAN', 'Tangent of'], + ['%{BKY_MATH_TRIG_ASIN}', 'ASIN', 'Arc sine of'], + ['%{BKY_MATH_TRIG_ACOS}', 'ACOS', 'Arc cosine of'], + ['%{BKY_MATH_TRIG_ATAN}', 'ATAN', 'Arc tangent of'], ], }, { @@ -144,13 +146,14 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'CONSTANT', + 'ariaTypeName': 'Math constant', 'options': [ - ['\u03c0', 'PI'], - ['e', 'E'], - ['\u03c6', 'GOLDEN_RATIO'], - ['sqrt(2)', 'SQRT2'], - ['sqrt(\u00bd)', 'SQRT1_2'], - ['\u221e', 'INFINITY'], + ['\u03c0', 'PI', 'Pi'], + ['e', 'E', "Euler's number"], + ['\u03c6', 'GOLDEN_RATIO', 'Golden ratio'], + ['sqrt(2)', 'SQRT2', 'Square root of 2'], + ['sqrt(\u00bd)', 'SQRT1_2', 'Square root of one half'], + ['\u221e', 'INFINITY', 'Infinity'], ], }, ], @@ -174,14 +177,15 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'PROPERTY', + 'ariaTypeName': 'Number comparison', 'options': [ - ['%{BKY_MATH_IS_EVEN}', 'EVEN'], - ['%{BKY_MATH_IS_ODD}', 'ODD'], - ['%{BKY_MATH_IS_PRIME}', 'PRIME'], - ['%{BKY_MATH_IS_WHOLE}', 'WHOLE'], - ['%{BKY_MATH_IS_POSITIVE}', 'POSITIVE'], - ['%{BKY_MATH_IS_NEGATIVE}', 'NEGATIVE'], - ['%{BKY_MATH_IS_DIVISIBLE_BY}', 'DIVISIBLE_BY'], + ['%{BKY_MATH_IS_EVEN}', 'EVEN', 'Is even'], + ['%{BKY_MATH_IS_ODD}', 'ODD', 'Is odd'], + ['%{BKY_MATH_IS_PRIME}', 'PRIME', 'Is prime'], + ['%{BKY_MATH_IS_WHOLE}', 'WHOLE', 'Is whole number'], + ['%{BKY_MATH_IS_POSITIVE}', 'POSITIVE', 'Is positive'], + ['%{BKY_MATH_IS_NEGATIVE}', 'NEGATIVE', 'Is negative'], + ['%{BKY_MATH_IS_DIVISIBLE_BY}', 'DIVISIBLE_BY', 'Is divisible by'], ], }, ], @@ -223,10 +227,11 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', + 'ariaTypeName': 'Rounding operation', 'options': [ - ['%{BKY_MATH_ROUND_OPERATOR_ROUND}', 'ROUND'], - ['%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}', 'ROUNDUP'], - ['%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}', 'ROUNDDOWN'], + ['%{BKY_MATH_ROUND_OPERATOR_ROUND}', 'ROUND', 'Round to nearest'], + ['%{BKY_MATH_ROUND_OPERATOR_ROUNDUP}', 'ROUNDUP', 'Round up'], + ['%{BKY_MATH_ROUND_OPERATOR_ROUNDDOWN}', 'ROUNDDOWN', 'Round down'], ], }, { @@ -250,15 +255,20 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'OP', + 'ariaTypeName': 'List operation', 'options': [ - ['%{BKY_MATH_ONLIST_OPERATOR_SUM}', 'SUM'], - ['%{BKY_MATH_ONLIST_OPERATOR_MIN}', 'MIN'], - ['%{BKY_MATH_ONLIST_OPERATOR_MAX}', 'MAX'], - ['%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}', 'AVERAGE'], - ['%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}', 'MEDIAN'], - ['%{BKY_MATH_ONLIST_OPERATOR_MODE}', 'MODE'], - ['%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}', 'STD_DEV'], - ['%{BKY_MATH_ONLIST_OPERATOR_RANDOM}', 'RANDOM'], + ['%{BKY_MATH_ONLIST_OPERATOR_SUM}', 'SUM', 'Sum'], + ['%{BKY_MATH_ONLIST_OPERATOR_MIN}', 'MIN', 'Minimum'], + ['%{BKY_MATH_ONLIST_OPERATOR_MAX}', 'MAX', 'Maximum'], + ['%{BKY_MATH_ONLIST_OPERATOR_AVERAGE}', 'AVERAGE', 'Average'], + ['%{BKY_MATH_ONLIST_OPERATOR_MEDIAN}', 'MEDIAN', 'Median'], + ['%{BKY_MATH_ONLIST_OPERATOR_MODE}', 'MODE', 'Mode'], + [ + '%{BKY_MATH_ONLIST_OPERATOR_STD_DEV}', + 'STD_DEV', + 'Standard deviation', + ], + ['%{BKY_MATH_ONLIST_OPERATOR_RANDOM}', 'RANDOM', 'Random value'], ], }, { diff --git a/blocks/text.ts b/blocks/text.ts index a7ad5374ac4..b002eef3111 100644 --- a/blocks/text.ts +++ b/blocks/text.ts @@ -142,9 +142,10 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'END', + 'ariaTypeName': 'Search direction', 'options': [ - ['%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', 'FIRST'], - ['%{BKY_TEXT_INDEXOF_OPERATOR_LAST}', 'LAST'], + ['%{BKY_TEXT_INDEXOF_OPERATOR_FIRST}', 'FIRST', 'First'], + ['%{BKY_TEXT_INDEXOF_OPERATOR_LAST}', 'LAST', 'Last'], ], }, { @@ -171,12 +172,13 @@ export const blocks = createBlockDefinitionsFromJsonArray([ { 'type': 'field_dropdown', 'name': 'WHERE', + 'ariaTypeName': 'Search operation', 'options': [ - ['%{BKY_TEXT_CHARAT_FROM_START}', 'FROM_START'], - ['%{BKY_TEXT_CHARAT_FROM_END}', 'FROM_END'], - ['%{BKY_TEXT_CHARAT_FIRST}', 'FIRST'], - ['%{BKY_TEXT_CHARAT_LAST}', 'LAST'], - ['%{BKY_TEXT_CHARAT_RANDOM}', 'RANDOM'], + ['%{BKY_TEXT_CHARAT_FROM_START}', 'FROM_START', 'From string start'], + ['%{BKY_TEXT_CHARAT_FROM_END}', 'FROM_END', 'From string end'], + ['%{BKY_TEXT_CHARAT_FIRST}', 'FIRST', 'First character'], + ['%{BKY_TEXT_CHARAT_LAST}', 'LAST', 'Last character'], + ['%{BKY_TEXT_CHARAT_RANDOM}', 'RANDOM', 'Random character'], ], }, ], diff --git a/core/block_svg.ts b/core/block_svg.ts index 133fc609641..0d6b9445b16 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -244,8 +244,8 @@ export class BlockSvg ); } - private computeAriaLabel(): string { - const {blockSummary, inputCount} = buildBlockSummary(this); + computeAriaLabel(verbose: boolean = false): string { + const {blockSummary, inputCount} = buildBlockSummary(this, verbose); const inputSummary = inputCount ? ` ${inputCount} ${inputCount > 1 ? 'inputs' : 'input'}` : ''; @@ -2051,7 +2051,7 @@ interface BlockSummary { inputCount: number; } -function buildBlockSummary(block: BlockSvg): BlockSummary { +function buildBlockSummary(block: BlockSvg, verbose: boolean): BlockSummary { let inputCount = 0; function recursiveInputSummary( block: BlockSvg, @@ -2066,7 +2066,7 @@ function buildBlockSummary(block: BlockSvg): BlockSummary { if (field.EDITABLE && !field.isFullBlockField() && !isNestedInput) { inputCount++; } - return [field.getText() ?? field.getValue()]; + return field.computeAriaLabel(verbose); }); if ( input.isVisible() && diff --git a/core/field.ts b/core/field.ts index ccbd3442275..5a9fcdc0fef 100644 --- a/core/field.ts +++ b/core/field.ts @@ -271,8 +271,51 @@ export abstract class Field } } - getAriaName(): string | null { - return this.config?.ariaName ?? null; + /** + * Gets a an ARIA-friendly label representation of this field's type. + * + * @returns An ARIA representation of the field's type or null if it is + * unspecified. + */ + getAriaTypeName(): string | null { + return this.config?.ariaTypeName ?? null; + } + + /** + * Gets a an ARIA-friendly label representation of this field's value. + * + * @returns An ARIA representation of the field's value. + */ + abstract getAriaValue(): string; + + /** + * Computes a descriptive ARIA label to represent this field with configurable + * verbosity. + * + * A 'verbose' label includes type information, if available, whereas a + * non-verbose label only contains the field's value. + * + * Note that this will always return the latest representation of the field's + * label which may differ from any previously set ARIA label for the field + * itself. Implementations are largely responsible for ensuring that the + * field's ARIA label is set correctly at relevant moments in the field's + * lifecycle (such as when its value changes). + * + * Finally, it is never guaranteed that implementations use the label returned + * by this method for their actual ARIA label. Some implementations may rely + * on other context to convey information like the field's value. Example: + * checkboxes represent their checked/non-checked status (i.e. value) through + * a separate ARIA property. + * + * @param verbose Whether to include the field's type information in the + * returned label, if available. + */ + computeAriaLabel(verbose: boolean = false): string { + const components: Array = [this.getAriaValue()]; + if (verbose) { + components.push(this.getAriaTypeName()); + } + return components.filter((item) => item !== null).join(', '); } /** @@ -1429,7 +1472,7 @@ export interface FieldConfig { type: string; name?: string; tooltip?: string; - ariaName?: string; + ariaTypeName?: string; } /** diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index df07168a7a2..aecead2e80c 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -116,10 +116,18 @@ export class FieldCheckbox extends Field { this.recomputeAria(); } + override getAriaValue(): string { + return this.value_ ? 'checked' : 'not checked'; + } + private recomputeAria() { const element = this.getFocusableElement(); aria.setRole(element, aria.Role.CHECKBOX); - aria.setState(element, aria.State.LABEL, this.getAriaName() ?? 'Checkbox'); + aria.setState( + element, + aria.State.LABEL, + this.getAriaTypeName() ?? 'Checkbox', + ); aria.setState(element, aria.State.CHECKED, !!this.value_); } diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 12ef0c27f03..878deb6ec3e 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -202,6 +202,10 @@ export class FieldDropdown extends Field { this.recomputeAria(); } + override getAriaValue(): string { + return this.computeLabelForOption(this.selectedOption); + } + protected recomputeAria() { if (!this.fieldGroup_) return; // There's no element to set currently. const element = this.getFocusableElement(); @@ -214,14 +218,7 @@ export class FieldDropdown extends Field { aria.clearState(element, aria.State.CONTROLS); } - const label = [ - this.computeLabelForOption(this.selectedOption), - this.getAriaName(), - ] - .filter((item) => !!item) - .join(', '); - - aria.setState(element, aria.State.LABEL, label); + aria.setState(element, aria.State.LABEL, super.computeAriaLabel(true)); } /** diff --git a/core/field_image.ts b/core/field_image.ts index 2b5a3139c71..b7aaf5e06bf 100644 --- a/core/field_image.ts +++ b/core/field_image.ts @@ -132,6 +132,10 @@ export class FieldImage extends Field { } } + override getAriaValue(): string { + return this.altText; + } + /** * Create the block UI for this image. */ @@ -159,11 +163,7 @@ export class FieldImage extends Field { if (this.isClickable()) { this.imageElement.style.cursor = 'pointer'; aria.setRole(element, aria.Role.BUTTON); - - const label = [this.altText, this.getAriaName()] - .filter((item) => !!item) - .join(', '); - aria.setState(element, aria.State.LABEL, label); + aria.setState(element, aria.State.LABEL, super.computeAriaLabel(true)); } else { // The field isn't navigable unless it's clickable. aria.setRole(element, aria.Role.PRESENTATION); diff --git a/core/field_input.ts b/core/field_input.ts index 244c6da4269..45fd99c529c 100644 --- a/core/field_input.ts +++ b/core/field_input.ts @@ -183,18 +183,18 @@ export abstract class FieldInput extends Field< this.recomputeAriaLabel(); } + override getAriaValue(): string { + // TODO: Figure out if this is actually sufficiently correct for conversion + null cases (probably not). + return String(this.getValue()); + } + /** * Updates the ARIA label for this field. */ protected recomputeAriaLabel() { if (!this.fieldGroup_) return; - const element = this.getFocusableElement(); - const label = [this.getValue(), this.getAriaName()] - .filter((item) => item !== null) - .join(', '); - - aria.setState(element, aria.State.LABEL, label); + aria.setState(element, aria.State.LABEL, super.computeAriaLabel()); } override isFullBlockField(): boolean { diff --git a/core/field_label.ts b/core/field_label.ts index d89e397f9c4..488eba952d3 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -118,6 +118,10 @@ export class FieldLabel extends Field { super.setValue(newValue, fireChangeEvent); } + override getAriaValue(): string { + return String(this.getValue()); + } + /** * Construct a FieldLabel from a JSON arg object, * dereferencing any string table references. diff --git a/core/shortcut_items.ts b/core/shortcut_items.ts index f8c95500770..838756beb25 100644 --- a/core/shortcut_items.ts +++ b/core/shortcut_items.ts @@ -16,6 +16,7 @@ import {isDeletable as isIDeletable} from './interfaces/i_deletable.js'; import {isDraggable} from './interfaces/i_draggable.js'; import {IFocusableNode} from './interfaces/i_focusable_node.js'; import {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js'; +import {aria} from './utils.js'; import {Coordinate} from './utils/coordinate.js'; import {KeyCodes} from './utils/keycodes.js'; import {Rect} from './utils/rect.js'; @@ -33,6 +34,8 @@ export enum names { PASTE = 'paste', UNDO = 'undo', REDO = 'redo', + READ_FULL_BLOCK_SUMMARY = 'read_full_block_summary', + READ_BLOCK_PARENT_SUMMARY = 'read_block_parent_summary', } /** @@ -386,6 +389,77 @@ export function registerRedo() { ShortcutRegistry.registry.register(redoShortcut); } +// A version of solving the 'where am I?' problem. Read out a more detailed +// summary of the current selected block. +export function registerReadFullBlockSummary() { + const ctrlShiftI = ShortcutRegistry.registry.createSerializedKey(KeyCodes.I, [ + KeyCodes.CTRL, + KeyCodes.SHIFT, + ]); + const metaShiftI = ShortcutRegistry.registry.createSerializedKey(KeyCodes.I, [ + KeyCodes.META, + KeyCodes.SHIFT, + ]); + const readFullBlockSummaryShortcut: KeyboardShortcut = { + name: names.READ_FULL_BLOCK_SUMMARY, + preconditionFn(workspace) { + return ( + !workspace.isDragging() && + !getFocusManager().ephemeralFocusTaken() && + !!getFocusManager().getFocusedNode() && + getFocusManager().getFocusedNode() instanceof BlockSvg + ); + }, + callback(workspace, e) { + const selectedBlock = getFocusManager().getFocusedNode() as BlockSvg; + const blockSummary = selectedBlock.computeAriaLabel(true); + aria.announceDynamicAriaState(`Current block: ${blockSummary}`); + e.preventDefault(); + return true; + }, + keyCodes: [ctrlShiftI, metaShiftI], + }; + ShortcutRegistry.registry.register(readFullBlockSummaryShortcut); +} + +// A version of solving the 'where am I?' problem. Read the current block's +// parent block. +export function registerReadBlockParentSummary() { + const ctrlShiftP = ShortcutRegistry.registry.createSerializedKey(KeyCodes.P, [ + KeyCodes.CTRL, + KeyCodes.SHIFT, + ]); + const metaShiftP = ShortcutRegistry.registry.createSerializedKey(KeyCodes.P, [ + KeyCodes.META, + KeyCodes.SHIFT, + ]); + const readBlockParentSummaryShortcut: KeyboardShortcut = { + name: names.READ_BLOCK_PARENT_SUMMARY, + preconditionFn(workspace) { + return ( + !workspace.isDragging() && + !getFocusManager().ephemeralFocusTaken() && + !!getFocusManager().getFocusedNode() && + getFocusManager().getFocusedNode() instanceof BlockSvg + ); + }, + callback(workspace, e) { + const selectedBlock = getFocusManager().getFocusedNode() as BlockSvg; + const parentBlock = selectedBlock.getParent(); + if (parentBlock) { + const blockSummary = parentBlock.computeAriaLabel(true); + aria.announceDynamicAriaState(`Parent block: ${blockSummary}`); + } else { + aria.announceDynamicAriaState('Current block has no parent'); + } + e.preventDefault(); + return true; + }, + keyCodes: [ctrlShiftP, metaShiftP], + }; + ShortcutRegistry.registry.register(readBlockParentSummaryShortcut); +} + /** * Registers all default keyboard shortcut item. This should be called once per * instance of KeyboardShortcutRegistry. @@ -400,6 +474,8 @@ export function registerDefaultShortcuts() { registerPaste(); registerUndo(); registerRedo(); + registerReadFullBlockSummary(); + registerReadBlockParentSummary(); } registerDefaultShortcuts();