Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Documentation/blueprints/application-hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ change slice **opens its dialog** instead of announcing the command:
- The button navigates to the dialog destination `sliceDialogDestination(resolved)` - the template `sliceDialogFor` picked, with the
`slice`, `command` and `title` as parameters. The host renders `dialogs[template]` when the application authors one (the
`core:sliceDialog` component, or its own element, reading `currentDialogParameters`), and the built-in **`SliceDialogView`** otherwise.
- The dialog is a centred modal card: the button title, the subtitle `State Change · <Slice>`, a labelled field per command property,
- The dialog is a centred modal card: the button title, a labelled field per command property,
and `Cancel` with a submit button labelled with the title. The first field takes focus, Tab stays inside, Escape cancels, and focus
goes back to the button. Required fields are checked before anything is invoked.
- The fields come from the command, through `SliceCommandFieldsProvider` (`fieldsFor(command, slice)` returns `{ name, type, label?, required? }`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('when previewing a State Change slice', () => {
});

it('should open the task dialog of the slice and draw nothing of the page of the feature behind it', () => [dialog()!.getAttribute('aria-label'), document.querySelector('[data-page="orders"]'), document.querySelector('.layout-topbar')].should.deep.equal(['Place order', null, null]));
it('should derive the dialog from the slice', () => [screen.getByRole('heading', { name: 'Place order' }).textContent, document.querySelector('.scene-slice-dialog-subtitle')!.textContent].should.deep.equal(['Place order', 'State Change · PlaceOrder']));
it('should derive the dialog from the slice', () => [screen.getByRole('heading', { name: 'Place order' }).textContent, document.querySelector('[data-scene-slice-dialog]')!.getAttribute('data-scene-command')].should.deep.equal(['Place order', 'PlaceOrder']));
it('should fill the fields from the active sample data pack', () => [field('Customer').value, field('Product').value, field('Quantity').value].should.deep.equal(['Fjordline AS', 'Scanner', '4']));
it('should report the slice in the breadcrumb', () => breadcrumbs.should.deep.equal([['Sales', 'Orders', 'Place order']]));
it('should have a Cancel button and a button titled like the slice', () => [screen.getByRole('button', { name: 'Cancel' }).textContent, document.querySelector('.scene-slice-dialog-submit')!.textContent].should.deep.equal(['Cancel', 'Place order']));
Expand Down
7 changes: 0 additions & 7 deletions Source/JavaScript/blueprint.default/shell/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -1198,13 +1198,6 @@
font-size: 1.35rem;
}

.scene-slice-dialog-subtitle {
margin: 0.25rem 0 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.8rem;
color: var(--scene-text-muted-color, #6b7280);
}

.scene-slice-dialog-fields {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ describe('when a toolbar button opens a slice as a task dialog', () => {

it('should open a modal dialog named by the button title', () =>
[dialog()!.getAttribute('aria-modal'), dialog()!.getAttribute('aria-label')].should.deep.equal(['true', 'New order']));
it('should show the title and the slice as the subtitle', () =>
[screen.getByRole('heading', { name: 'New order' }).textContent, document.querySelector('.scene-slice-dialog-subtitle')!.textContent].should.deep.equal(['New order', 'State Change · PlaceOrder']));
it('should show the title', () => screen.getByRole('heading', { name: 'New order' }).textContent!.should.equal('New order'));
it('should not show what the slice is called in the model', () => (document.querySelector('.scene-slice-dialog')!.textContent ?? '').should.not.contain('State Change'));
it('should show a labelled field for each command property', () =>
Array.from(document.querySelectorAll('.scene-slice-dialog-field label')).map(label => label.textContent).should.deep.equal(['Customer *', 'Product', 'Quantity', 'Express delivery']));
it('should focus the first field', () => document.activeElement!.getAttribute('name')!.should.equal('customer'));
Expand Down
16 changes: 4 additions & 12 deletions Source/JavaScript/react/slices/SliceDialogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import { FormEvent, KeyboardEvent, useContext, useEffect, useId, useMemo, useRef, useState } from 'react';
import { ApplicationStructureIndex } from '@cratis/scene.engine';
import { SceneNavigationContextInternal } from '../navigation/SceneNavigationContext';
import { useSceneNavigationModel } from '../navigation/region/SceneNavigationModelContext';
import { InvocationResult } from './InvocationResult';
import { InvocationValidationError } from './InvocationValidationError';
import { SliceCommandField } from './SliceCommandField';
Expand Down Expand Up @@ -70,14 +68,15 @@ export function slicePayload(fields: SliceCommandField[], values: Record<string,
}

/**
* The task dialog of a toolbar slice button (TB4): a modal card with the button's title, the slice as its subtitle, a labelled
* field for each command property, and Cancel and a submit button labelled with the title.
* The task dialog of a toolbar slice button (TB4): a modal card with the button's title, a labelled field for each command
* property, and Cancel and a submit button labelled with the title. The slice is what the dialog is for, but that is the model's word,
* not the person's: it is kept on the dialog's `data-scene-slice-dialog` attribute, never shown.
*
* - The first field takes focus when it opens, Tab stays inside, Escape cancels, and focus returns to the button that opened it.
* - Submit validates required fields, then calls the `SliceInvoker` in scope - real, dry-run or the announcing default. The
* problems it reports are shown against their fields (`aria-invalid`, `aria-describedby`) or for the form; success closes the
* dialog and hands the result to the host (`dialogResult`).
* - The fields come from the `SliceCommandFieldsProvider`; the subtitle names the slice from the navigation model.
* - The fields come from the `SliceCommandFieldsProvider`.
*
* Inside a navigation host it is the content of the dialog a toolbar button opens; `core:sliceDialog` registers it so a host can
* author the dialog template with it.
Expand All @@ -91,13 +90,7 @@ export function SliceDialogView({ slice: sliceProp, command: commandProp, title:

const invoker = useSliceInvoker();
const fieldsFor = useSliceCommandFields();
const model = useSceneNavigationModel();
const fields = useMemo(() => fieldsFor?.(command, slice) ?? [], [command, fieldsFor, slice]);
const sliceName = useMemo(() => {
if (!model) return slice.split('.').pop() ?? slice;
const index = model.structure instanceof ApplicationStructureIndex ? model.structure : new ApplicationStructureIndex(model.structure);
return index.findSlice(slice)?.id ?? slice.split('.').pop() ?? slice;
}, [model, slice]);

const [values, setValues] = useState<Record<string, string | boolean>>(() => initialValuesOf(fields));
const [errors, setErrors] = useState<InvocationValidationError[]>([]);
Expand Down Expand Up @@ -161,7 +154,6 @@ export function SliceDialogView({ slice: sliceProp, command: commandProp, title:
return <form ref={form} className={`scene-slice-dialog scene-slice-dialog-${presentation}`} data-scene-presentation={presentation} data-scene-slice-dialog={slice} data-scene-command={command} noValidate onSubmit={submit} onKeyDown={onKeyDown}>
<header className='scene-slice-dialog-header'>
<h2 className='scene-slice-dialog-title'>{title}</h2>
<p className='scene-slice-dialog-subtitle'>State Change · {sliceName}</p>
</header>
<div className='scene-slice-dialog-fields'>
{fields.map(field => {
Expand Down
Loading