Skip to content

Commit

Permalink
TINY-10971: introduce optional label for property (#9681)
Browse files Browse the repository at this point in the history
* TINY-10971: Experimented with for name

* TINY-10971: clean duplicated code and add test

* TINY-10971: cleaning

* TINY-10971: changie

* TINY-10971: lint

* TINY-10971: improve phrasing in changelog

Co-authored-by: Andrew Herron <thespyder@programmer.net>

---------

Co-authored-by: Spocke <spocke@moxiecode.com>
Co-authored-by: Andrew Herron <thespyder@programmer.net>
  • Loading branch information
3 people authored Jun 5, 2024
1 parent 054671e commit 70cff12
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 57 deletions.
7 changes: 7 additions & 0 deletions .changes/unreleased/bridge-TINY-10971-2024-06-04.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
project: bridge
kind: Added
body: Added `for` attribute to component schema and as an optinal parameter for label
component.
time: 2024-06-04T18:15:54.417924+02:00
custom:
Issue: TINY-10971
7 changes: 7 additions & 0 deletions .changes/unreleased/tinymce-TINY-10971-2024-06-04.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
project: tinymce
kind: Added
body: Added `for` option to dialog label components to improve accessibility. The
value must be another component on the same dialog.
time: 2024-06-04T18:18:38.295757+02:00
custom:
Issue: TINY-10971
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FieldProcessor, FieldSchema } from '@ephox/boulder';
import { Optional } from '@ephox/katamari';

import * as ComponentSchema from '../../core/ComponentSchema';
import { BodyComponent, BodyComponentSpec } from './BodyComponent';
Expand All @@ -10,18 +11,21 @@ export interface LabelSpec {
label: string;
items: BodyComponentSpec[];
align?: Alignment;
for?: string;
}

export interface Label {
type: 'label';
label: string;
items: BodyComponent[];
align: Alignment;
for: Optional<string>;
}

export const createLabelFields = (itemsField: FieldProcessor): FieldProcessor[] => [
ComponentSchema.type,
ComponentSchema.label,
itemsField,
FieldSchema.defaultedStringEnum('align', 'start', [ 'start', 'center', 'end' ])
FieldSchema.defaultedStringEnum('align', 'start', [ 'start', 'center', 'end' ]),
FieldSchema.optionString('for')
];
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AlloyComponent, AlloySpec } from '@ephox/alloy';
import { Dialog, Menu } from '@ephox/bridge';
import { Cell, Result } from '@ephox/katamari';
import { Cell, Optional, Result } from '@ephox/katamari';

import Editor from 'tinymce/core/api/Editor';
import I18n, { TranslatedString, Untranslated } from 'tinymce/core/api/util/I18n';
Expand Down Expand Up @@ -82,11 +82,13 @@ const init = (lazySinks: { popup: () => Result<AlloyComponent, string>; dialog:
setContextMenuState
};

const getCompByName = (_name: string) => Optional.none();

const popupBackstage: UiFactoryBackstage = {
...commonBackstage,
shared: {
...commonBackstage.shared,
interpreter: (s) => UiFactory.interpretWithoutForm(s, {}, popupBackstage),
interpreter: (s) => UiFactory.interpretWithoutForm(s, {}, popupBackstage, getCompByName),
getSink: lazySinks.popup
}
};
Expand All @@ -95,7 +97,7 @@ const init = (lazySinks: { popup: () => Result<AlloyComponent, string>; dialog:
...commonBackstage,
shared: {
...commonBackstage.shared,
interpreter: (s) => UiFactory.interpretWithoutForm(s, {}, dialogBackstage),
interpreter: (s) => UiFactory.interpretWithoutForm(s, {}, dialogBackstage, getCompByName),
getSink: lazySinks.dialog
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddEventsBehaviour, AlloyEvents, Behaviour, Form as AlloyForm, Keying, Memento, NativeEvents, SimpleSpec } from '@ephox/alloy';
import { AddEventsBehaviour, AlloyEvents, Behaviour, Form as AlloyForm, Keying, Memento, NativeEvents, SimpleSpec, AlloyComponent } from '@ephox/alloy';
import { Dialog } from '@ephox/bridge';
import { Arr, Fun, Optional } from '@ephox/katamari';

Expand All @@ -12,7 +12,7 @@ import { dialogFocusShiftedChannel } from '../window/DialogChannels';

export type BodyPanelSpec = Omit<Dialog.Panel, 'type'>;

const renderBodyPanel = (spec: BodyPanelSpec, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage): SimpleSpec => {
const renderBodyPanel = (spec: BodyPanelSpec, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>): SimpleSpec => {
const memForm = Memento.record(
AlloyForm.sketch((parts) => ({
dom: {
Expand All @@ -21,7 +21,7 @@ const renderBodyPanel = (spec: BodyPanelSpec, dialogData: Dialog.DialogData, bac
},
// All of the items passed through the form need to be put through the interpreter
// with their form part preserved.
components: Arr.map(spec.items, (item) => interpretInForm(parts, item, dialogData, backstage))
components: Arr.map(spec.items, (item) => interpretInForm(parts, item, dialogData, backstage, getCompByName))
}))
);

Expand Down
29 changes: 21 additions & 8 deletions modules/tinymce/src/themes/silver/main/ts/ui/dialog/Label.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { AlloySpec, Behaviour, GuiFactory, Keying, Replacing, SimpleSpec } from '@ephox/alloy';
import { AddEventsBehaviour, AlloyComponent, AlloyEvents, Behaviour, GuiFactory, Keying, Memento, Replacing, SimpleSpec } from '@ephox/alloy';
import { Dialog } from '@ephox/bridge';
import { Arr, Optional } from '@ephox/katamari';
import { Arr, Id, Optional } from '@ephox/katamari';
import { Attribute } from '@ephox/sugar';

import { UiFactoryBackstageShared } from '../../backstage/Backstage';
import { ComposingConfigs } from '../alien/ComposingConfigs';
import * as RepresentingConfigs from '../alien/RepresentingConfigs';

type LabelSpec = Omit<Dialog.Label, 'type'>;

export const renderLabel = (spec: LabelSpec, backstageShared: UiFactoryBackstageShared): SimpleSpec => {
export const renderLabel = (spec: LabelSpec, backstageShared: UiFactoryBackstageShared, getCompByName: (name: string) => Optional<AlloyComponent>): SimpleSpec => {
const baseClass = 'tox-label';
const centerClass = spec.align === 'center' ? [ `${baseClass}--center` ] : [];
const endClass = spec.align === 'end' ? [ `${baseClass}--end` ] : [];

const label: AlloySpec = {
const label = Memento.record({
dom: {
tag: 'label',
classes: [ baseClass, ...centerClass, ...endClass ]
},
components: [
GuiFactory.text(backstageShared.providers.translate(spec.label))
]
};
});

const comps = Arr.map(spec.items, backstageShared.interpreter);
return {
Expand All @@ -30,7 +30,7 @@ export const renderLabel = (spec: LabelSpec, backstageShared: UiFactoryBackstage
classes: [ 'tox-form__group' ]
},
components: [
label,
label.asSpec(),
...comps
],
behaviours: Behaviour.derive([
Expand All @@ -39,7 +39,20 @@ export const renderLabel = (spec: LabelSpec, backstageShared: UiFactoryBackstage
RepresentingConfigs.domHtml(Optional.none()),
Keying.config({
mode: 'acyclic'
})
}),
AddEventsBehaviour.config('label', [
AlloyEvents.runOnAttached((comp) => {
spec.for.each((name) => {
getCompByName(name).each((target) => {
label.getOpt(comp).each((labelComp) => {
const id = Attribute.get(target.element, 'id') ?? Id.generate('form-field');
Attribute.set(target.element, 'id', id);
Attribute.set(labelComp.element, 'for', id);
});
});
});
})
]),
])
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type TabData = Record<string, any>;

type TabPanelSpec = Omit<Dialog.TabPanel, 'type'>;

export const renderTabPanel = (spec: TabPanelSpec, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage): SketchSpec => {
export const renderTabPanel = (spec: TabPanelSpec, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>): SketchSpec => {
const storedValue = Cell<TabData>({ });

const updateDataWithForm = (form: AlloyComponent): void => {
Expand Down Expand Up @@ -58,7 +58,7 @@ export const renderTabPanel = (spec: TabPanelSpec, dialogData: Dialog.DialogData
tag: 'div',
classes: [ 'tox-form' ]
},
components: Arr.map(tab.items, (item) => interpretInForm(parts, item, dialogData, backstage)),
components: Arr.map(tab.items, (item) => interpretInForm(parts, item, dialogData, backstage, getCompByName)),
formBehaviours: Behaviour.derive([
Keying.config({
mode: 'acyclic',
Expand Down
32 changes: 16 additions & 16 deletions modules/tinymce/src/themes/silver/main/ts/ui/general/UiFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AlloyParts, AlloySpec, FormTypes, SimpleOrSketchSpec } from '@ephox/alloy';
import { AlloyComponent, AlloyParts, AlloySpec, FormTypes, SimpleOrSketchSpec } from '@ephox/alloy';
import { Dialog } from '@ephox/bridge/';
import { Fun, Merger, Obj, Optional } from '@ephox/katamari';

Expand Down Expand Up @@ -29,22 +29,22 @@ import { renderHtmlPanel } from './HtmlPanel';

/* eslint-disable no-console */

export type FormPartRenderer<T extends Dialog.BodyComponent> = (parts: FormTypes.FormParts, spec: T, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage) => AlloySpec;
export type NoFormRenderer<T extends Dialog.BodyComponent, U> = (spec: T, backstage: UiFactoryBackstage, data: Optional<U>) => AlloySpec;
export type FormPartRenderer<T extends Dialog.BodyComponent> = (parts: FormTypes.FormParts, spec: T, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>) => AlloySpec;
export type NoFormRenderer<T extends Dialog.BodyComponent, U> = (spec: T, backstage: UiFactoryBackstage, data: Optional<U>, getCompByName: (name: string) => Optional<AlloyComponent>) => AlloySpec;

const make = <T extends Dialog.BodyComponent, U = unknown>(render: NoFormRenderer<T, U>): FormPartRenderer<T> => {
return (parts, spec, dialogData, backstage) =>
return (parts, spec, dialogData, backstage, getCompByName) =>
Obj.get(spec as Record<string, any>, 'name').fold(
() => render(spec, backstage, Optional.none()),
(fieldName) => parts.field(fieldName, render(spec, backstage, Obj.get(dialogData, fieldName)) as SimpleOrSketchSpec)
() => render(spec, backstage, Optional.none(), getCompByName),
(fieldName) => parts.field(fieldName, render(spec, backstage, Obj.get(dialogData, fieldName), getCompByName) as SimpleOrSketchSpec)
);
};

const makeIframe = (render: NoFormRenderer<Dialog.Iframe, string>): FormPartRenderer<Dialog.Iframe> => (parts, spec, dialogData, backstage) => {
const makeIframe = (render: NoFormRenderer<Dialog.Iframe, string>): FormPartRenderer<Dialog.Iframe> => (parts, spec, dialogData, backstage, getCompByName) => {
const iframeSpec = Merger.deepMerge(spec, {
source: 'dynamic'
});
return make(render)(parts, iframeSpec, dialogData, backstage);
return make(render)(parts, iframeSpec, dialogData, backstage, getCompByName);
};

const factories: Record<string, FormPartRenderer<any>> = {
Expand All @@ -53,7 +53,7 @@ const factories: Record<string, FormPartRenderer<any>> = {
alertbanner: make<Dialog.AlertBanner>((spec, backstage) => renderAlertBanner(spec, backstage.shared.providers)),
input: make<Dialog.Input, string>((spec, backstage, data) => renderInput(spec, backstage.shared.providers, data)),
textarea: make<Dialog.TextArea, string>((spec, backstage, data) => renderTextarea(spec, backstage.shared.providers, data)),
label: make<Dialog.Label>((spec, backstage) => renderLabel(spec, backstage.shared)),
label: make<Dialog.Label>((spec, backstage, _data, getCompByName) => renderLabel(spec, backstage.shared, getCompByName)),
iframe: makeIframe((spec, backstage, data) => renderIFrame(spec, backstage.shared.providers, data)),
button: make<Dialog.Button>((spec, backstage) => renderDialogButton(spec, backstage.shared.providers)),
checkbox: make<Dialog.Checkbox, boolean>((spec, backstage, data) => renderCheckbox(spec, backstage.shared.providers, data)),
Expand All @@ -80,31 +80,31 @@ const noFormParts: FormTypes.FormParts = {
record: Fun.constant([])
};

const interpretInForm = <T extends Dialog.BodyComponent>(parts: FormTypes.FormParts, spec: T, dialogData: Dialog.DialogData, oldBackstage: UiFactoryBackstage): AlloySpec => {
const interpretInForm = <T extends Dialog.BodyComponent>(parts: FormTypes.FormParts, spec: T, dialogData: Dialog.DialogData, oldBackstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>): AlloySpec => {
// Now, we need to update the backstage to use the parts variant.
const newBackstage = Merger.deepMerge(
oldBackstage,
{
// Add the interpreter based on the form parts.
shared: {
interpreter: (childSpec: T) => interpretParts(parts, childSpec, dialogData, newBackstage)
interpreter: (childSpec: T) => interpretParts(parts, childSpec, dialogData, newBackstage, getCompByName)
}
}
);

return interpretParts(parts, spec, dialogData, newBackstage);
return interpretParts(parts, spec, dialogData, newBackstage, getCompByName);
};

const interpretParts = <T extends Dialog.BodyComponent>(parts: FormTypes.FormParts, spec: T, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage): AlloySpec =>
const interpretParts = <T extends Dialog.BodyComponent>(parts: FormTypes.FormParts, spec: T, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>): AlloySpec =>
Obj.get(factories, spec.type).fold(
() => {
console.error(`Unknown factory type "${spec.type}", defaulting to container: `, spec);
return spec as unknown as AlloySpec;
},
(factory) => factory(parts, spec, dialogData, backstage)
(factory) => factory(parts, spec, dialogData, backstage, getCompByName)
);

const interpretWithoutForm = <T extends Dialog.BodyComponent>(spec: T, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage): AlloySpec =>
interpretParts(noFormParts, spec, dialogData, backstage);
const interpretWithoutForm = <T extends Dialog.BodyComponent>(spec: T, dialogData: Dialog.DialogData, backstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>): AlloySpec =>
interpretParts(noFormParts, spec, dialogData, backstage, getCompByName);

export { interpretInForm, interpretWithoutForm };
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { renderModalBody } from './SilverDialogBody';
import * as SilverDialogCommon from './SilverDialogCommon';
import * as SilverDialogEvents from './SilverDialogEvents';
import { renderModalFooter } from './SilverDialogFooter';
import { DialogAccess, getDialogApi } from './SilverDialogInstanceApi';
import * as SilverDialogInstanceApi from './SilverDialogInstanceApi';

interface RenderedDialog<T extends Dialog.DialogData> {
readonly dialog: AlloyComponent;
Expand All @@ -22,6 +22,8 @@ const renderDialog = <T extends Dialog.DialogData>(dialogInit: DialogManager.Dia

const dialogSize = Cell<Dialog.DialogSize>(internalDialog.size);

const getCompByName = (name: string) => SilverDialogInstanceApi.getCompByName(modalAccess, name);

const dialogSizeClasses = SilverDialogCommon.getDialogSizeClass(dialogSize.get()).toArray();

const updateState = (comp: AlloyComponent, incoming: DialogManager.DialogInit<T>) => {
Expand All @@ -33,7 +35,7 @@ const renderDialog = <T extends Dialog.DialogData>(dialogInit: DialogManager.Dia
const body = renderModalBody({
body: internalDialog.body,
initialData: internalDialog.initialData
}, dialogId, backstage);
}, dialogId, backstage, getCompByName);

const storedMenuButtons = SilverDialogCommon.mapMenuButtons(internalDialog.buttons);

Expand Down Expand Up @@ -68,7 +70,7 @@ const renderDialog = <T extends Dialog.DialogData>(dialogInit: DialogManager.Dia

const dialog: AlloyComponent = SilverDialogCommon.renderModalDialog(spec, dialogEvents, backstage);

const modalAccess = ((): DialogAccess => {
const modalAccess = ((): SilverDialogInstanceApi.DialogAccess => {
const getForm = (): AlloyComponent => {
const outerForm = ModalDialog.getBody(dialog);
return Composing.getCurrent(outerForm).getOr(outerForm);
Expand All @@ -89,7 +91,7 @@ const renderDialog = <T extends Dialog.DialogData>(dialogInit: DialogManager.Dia
})();

// TODO: Get the validator from the dialog state.
const instanceApi = getDialogApi<T>(modalAccess, extra.redial, objOfCells);
const instanceApi = SilverDialogInstanceApi.getDialogApi<T>(modalAccess, extra.redial, objOfCells);

return {
dialog,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ interface WindowBodySpec {

// ariaAttrs is being passed through to silver inline dialog
// from the WindowManager as a property of 'params'
const renderBody = (spec: WindowBodySpec, dialogId: string, contentId: Optional<string>, backstage: UiFactoryBackstage, ariaAttrs: boolean): SimpleSpec => {
const renderBody = (spec: WindowBodySpec, dialogId: string, contentId: Optional<string>, backstage: UiFactoryBackstage, ariaAttrs: boolean, getCompByName: (name: string) => Optional<AlloyComponent>): SimpleSpec => {
const renderComponents = (incoming: WindowBodySpec) => {
const body = incoming.body;
switch (body.type) {
case 'tabpanel': {
return [
renderTabPanel(body, incoming.initialData, backstage)
renderTabPanel(body, incoming.initialData, backstage, getCompByName)
];
}

default: {
return [
renderBodyPanel(body, incoming.initialData, backstage)
renderBodyPanel(body, incoming.initialData, backstage, getCompByName)
];
}
}
Expand Down Expand Up @@ -65,11 +65,11 @@ const renderBody = (spec: WindowBodySpec, dialogId: string, contentId: Optional<
};
};

const renderInlineBody = (spec: WindowBodySpec, dialogId: string, contentId: string, backstage: UiFactoryBackstage, ariaAttrs: boolean): SimpleSpec =>
renderBody(spec, dialogId, Optional.some(contentId), backstage, ariaAttrs);
const renderInlineBody = (spec: WindowBodySpec, dialogId: string, contentId: string, backstage: UiFactoryBackstage, ariaAttrs: boolean, getCompByName: (name: string) => Optional<AlloyComponent>): SimpleSpec =>
renderBody(spec, dialogId, Optional.some(contentId), backstage, ariaAttrs, getCompByName);

const renderModalBody = (spec: WindowBodySpec, dialogId: string, backstage: UiFactoryBackstage): AlloyParts.ConfiguredPart => {
const bodySpec = renderBody(spec, dialogId, Optional.none(), backstage, false);
const renderModalBody = (spec: WindowBodySpec, dialogId: string, backstage: UiFactoryBackstage, getCompByName: (name: string) => Optional<AlloyComponent>): AlloyParts.ConfiguredPart => {
const bodySpec = renderBody(spec, dialogId, Optional.none(), backstage, false, getCompByName);
return ModalDialog.parts.body(bodySpec);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,5 +164,6 @@ const getDialogApi = <T extends Dialog.DialogData>(
};

export {
getDialogApi
getDialogApi,
getCompByName
};
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const renderInlineDialog = <T extends Dialog.DialogData>(
const dialogContentId = Id.generate('dialog-content');
const internalDialog = dialogInit.internalDialog;

const getCompByName = (name: string) => SilverDialogInstanceApi.getCompByName(modalAccess, name);

const dialogSize = Cell<Dialog.DialogSize>(internalDialog.size);

const dialogSizeClass = SilverDialogCommon.getDialogSizeClass(dialogSize.get()).toArray();
Expand All @@ -61,7 +63,7 @@ const renderInlineDialog = <T extends Dialog.DialogData>(
SilverDialogBody.renderInlineBody({
body: internalDialog.body,
initialData: internalDialog.initialData,
}, dialogId, dialogContentId, backstage, ariaAttrs)
}, dialogId, dialogContentId, backstage, ariaAttrs, getCompByName)
);

const storagedMenuButtons = SilverDialogCommon.mapMenuButtons(internalDialog.buttons);
Expand Down Expand Up @@ -165,7 +167,7 @@ const renderInlineDialog = <T extends Dialog.DialogData>(
};

// TODO: Clean up the dupe between this (InlineDialog) and SilverDialog
const instanceApi = SilverDialogInstanceApi.getDialogApi<T>({
const modalAccess: SilverDialogInstanceApi.DialogAccess = {
getId: Fun.constant(dialogId),
getRoot: Fun.constant(dialog),
getFooter: () => optMemFooter.map((memFooter) => memFooter.get(dialog)),
Expand All @@ -175,7 +177,8 @@ const renderInlineDialog = <T extends Dialog.DialogData>(
return Composing.getCurrent(body).getOr(body);
},
toggleFullscreen
}, extra.redial, objOfCells);
};
const instanceApi = SilverDialogInstanceApi.getDialogApi<T>(modalAccess, extra.redial, objOfCells);

return {
dialog,
Expand Down
Loading

0 comments on commit 70cff12

Please sign in to comment.