import { Fun, Optional, Singleton } from '@ephox/katamari'; import * as ComponentStructure from '../../alien/ComponentStructure'; import * as AriaControls from '../../aria/AriaControls'; import * as MaxWidth from '../../positioning/layout/MaxWidth'; import type { Layouts } from '../../positioning/mode/Anchoring'; import * as Dismissal from '../../sandbox/Dismissal'; import * as Reposition from '../../sandbox/Reposition'; import * as FloatingToolbarButtonSchema from '../../ui/schema/FloatingToolbarButtonSchema'; import type { FloatingToolbarButtonApis, FloatingToolbarButtonDetail, FloatingToolbarButtonSketcher, FloatingToolbarButtonSpec } from '../../ui/types/FloatingToolbarButtonTypes'; import * as Behaviour from '../behaviour/Behaviour'; import { Coupling } from '../behaviour/Coupling'; import { Focusing } from '../behaviour/Focusing'; import { Keying } from '../behaviour/Keying'; import { Positioning } from '../behaviour/Positioning'; import { Receiving } from '../behaviour/Receiving'; import { Sandboxing } from '../behaviour/Sandboxing'; import { Toggling } from '../behaviour/Toggling'; import type { AlloyComponent } from '../component/ComponentApi'; import { SketchBehaviours } from '../component/SketchBehaviours'; import type { AlloySpec, SketchSpec } from '../component/SpecTypes'; import { Button } from './Button'; import * as Sketcher from './Sketcher'; import { Toolbar } from './Toolbar'; import type { CompositeSketchFactory } from './UiSketcher'; const shouldSkipFocus = Singleton.value(); const toggleWithoutFocusing = (button: AlloyComponent, externals: Record) => { shouldSkipFocus.set(true); toggle(button, externals); shouldSkipFocus.clear(); }; const toggle = (button: AlloyComponent, externals: Record) => { const toolbarSandbox = Coupling.getCoupled(button, 'toolbarSandbox'); if (Sandboxing.isOpen(toolbarSandbox)) { Sandboxing.close(toolbarSandbox); } else { Sandboxing.open(toolbarSandbox, externals.toolbar()); } }; const position = (button: AlloyComponent, toolbar: AlloyComponent, detail: FloatingToolbarButtonDetail, layouts: Layouts | undefined) => { const bounds = detail.getBounds.map((bounder) => bounder()); const sink = detail.lazySink(button).getOrDie(); Positioning.positionWithinBounds(sink, toolbar, { anchor: { type: 'hotspot', hotspot: button, layouts, overrides: { maxWidthFunction: MaxWidth.expandable() } } }, bounds); }; const setGroups = (button: AlloyComponent, toolbar: AlloyComponent, detail: FloatingToolbarButtonDetail, layouts: Layouts | undefined, groups: AlloySpec[]) => { Toolbar.setGroups(toolbar, groups); position(button, toolbar, detail, layouts); Toggling.on(button); }; const makeSandbox = (button: AlloyComponent, spec: FloatingToolbarButtonSpec, detail: FloatingToolbarButtonDetail) => { const ariaControls = AriaControls.manager(); const onOpen = (sandbox: AlloyComponent, toolbar: AlloyComponent) => { const skipFocus = shouldSkipFocus.get().getOr(false); detail.fetch().get((groups) => { setGroups(button, toolbar, detail, spec.layouts, groups); ariaControls.link(button.element); if (!skipFocus) { Keying.focusIn(toolbar); } }); }; const onClose = () => { // Toggle and focus the button Toggling.off(button); if (!shouldSkipFocus.get().getOr(false)) { Focusing.focus(button); } ariaControls.unlink(button.element); }; return { dom: { tag: 'div', attributes: { id: ariaControls.id } }, behaviours: Behaviour.derive( [ Keying.config({ mode: 'special', onEscape: (comp) => { Sandboxing.close(comp); return Optional.some(true); } }), Sandboxing.config({ onOpen, onClose, isPartOf: (container, data, queryElem): boolean => { return ComponentStructure.isPartOf(data, queryElem) || ComponentStructure.isPartOf(button, queryElem); }, getAttachPoint: () => { return detail.lazySink(button).getOrDie(); } }), Receiving.config({ channels: { ...Dismissal.receivingChannel({ isExtraPart: Fun.never, ...detail.fireDismissalEventInstead.map((fe) => ({ fireEventInstead: { event: fe.event }} as any)).getOr({ }) }), ...Reposition.receivingChannel({ doReposition: () => { Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')).each((toolbar) => { position(button, toolbar, detail, spec.layouts); }); } }) } }) ] ) }; }; const factory: CompositeSketchFactory = (detail, components, spec, externals): SketchSpec => ({ ...Button.sketch({ ...externals.button(), action: (button) => { toggle(button, externals); }, buttonBehaviours: SketchBehaviours.augment( { dump: externals.button().buttonBehaviours }, [ Coupling.config({ others: { toolbarSandbox: (button) => { return makeSandbox(button, spec, detail); } } }) ] ) }), apis: { setGroups: (button: AlloyComponent, groups: AlloySpec[]) => { Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')).each((toolbar) => { setGroups(button, toolbar, detail, spec.layouts, groups); }); }, reposition: (button: AlloyComponent) => { Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')).each((toolbar) => { position(button, toolbar, detail, spec.layouts); }); }, toggle: (button: AlloyComponent) => { toggle(button, externals); }, toggleWithoutFocusing: (button: AlloyComponent) => { toggleWithoutFocusing(button, externals); }, getToolbar: (button: AlloyComponent) => { return Sandboxing.getState(Coupling.getCoupled(button, 'toolbarSandbox')); }, isOpen: (button: AlloyComponent) => { return Sandboxing.isOpen(Coupling.getCoupled(button, 'toolbarSandbox')); } } }); const FloatingToolbarButton: FloatingToolbarButtonSketcher = Sketcher.composite({ name: 'FloatingToolbarButton', factory, configFields: FloatingToolbarButtonSchema.schema(), partFields: FloatingToolbarButtonSchema.parts(), apis: { setGroups: (apis, button, groups) => { apis.setGroups(button, groups); }, reposition: (apis, button) => { apis.reposition(button); }, toggle: (apis, button) => { apis.toggle(button); }, toggleWithoutFocusing: (apis, button) => { apis.toggleWithoutFocusing(button); }, getToolbar: (apis, button) => apis.getToolbar(button), isOpen: (apis, button) => apis.isOpen(button) } }); export { FloatingToolbarButton };