@lablup/ui-common 0.2.0-alpha.17 → 0.2.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/astryx/components/Board.doc.mjs +122 -0
- package/astryx/components/Board.tsx +3 -0
- package/astryx/components/Form.doc.mjs +1 -1
- package/astryx/docs/ui-common.doc.mjs +1 -1
- package/dist/assets/components/Board/Board.css +2 -0
- package/dist/assets/components/Form/FormItemVisual.css +1 -1
- package/dist/astryx/DateInput/tokens.stylex.d.ts +1 -0
- package/dist/astryx/DateInput/tokens.stylex.js +1 -0
- package/dist/astryx/Icon/IconSize.stylex.d.ts +1 -0
- package/dist/astryx/Icon/IconSize.stylex.js +1 -0
- package/dist/astryx/Indicator/indicator.markers.stylex.d.ts +1 -0
- package/dist/astryx/Indicator/indicator.markers.stylex.js +1 -0
- package/dist/astryx/Layer/layerAnimations.stylex.d.ts +1 -0
- package/dist/astryx/Layer/layerAnimations.stylex.js +1 -0
- package/dist/astryx/Layer/layerTextReset.stylex.d.ts +1 -0
- package/dist/astryx/Layer/layerTextReset.stylex.js +1 -0
- package/dist/astryx/Layer/layerViewportInset.stylex.d.ts +1 -0
- package/dist/astryx/Layer/layerViewportInset.stylex.js +1 -0
- package/dist/astryx/Layout/container.stylex.d.ts +1 -0
- package/dist/astryx/Layout/container.stylex.js +1 -0
- package/dist/astryx/Layout/edgeCompensation.stylex.d.ts +1 -0
- package/dist/astryx/Layout/edgeCompensation.stylex.js +1 -0
- package/dist/astryx/Layout/padding.stylex.d.ts +1 -0
- package/dist/astryx/Layout/padding.stylex.js +1 -0
- package/dist/astryx/Markdown/plugin-renderer.d.ts +1 -0
- package/dist/astryx/Markdown/plugin-renderer.js +1 -0
- package/dist/astryx/NavItem/navItemStyles.stylex.d.ts +1 -0
- package/dist/astryx/NavItem/navItemStyles.stylex.js +1 -0
- package/dist/astryx/Selector/selectorPresentation.stylex.d.ts +1 -0
- package/dist/astryx/Selector/selectorPresentation.stylex.js +1 -0
- package/dist/astryx/Stack/stack.stylex.d.ts +1 -0
- package/dist/astryx/Stack/stack.stylex.js +1 -0
- package/dist/astryx/Stack/stackItem.stylex.d.ts +1 -0
- package/dist/astryx/Stack/stackItem.stylex.js +1 -0
- package/dist/astryx/Text/text.stylex.d.ts +1 -0
- package/dist/astryx/Text/text.stylex.js +1 -0
- package/dist/astryx/theme/dataTokens.stylex.d.ts +1 -0
- package/dist/astryx/theme/dataTokens.stylex.js +1 -0
- package/dist/astryx/theme/neutral/fonts.css +3 -0
- package/dist/astryx/utils/focusOutline.stylex.d.ts +1 -0
- package/dist/astryx/utils/focusOutline.stylex.js +1 -0
- package/dist/astryx/utils/interactionOverlay.stylex.d.ts +1 -0
- package/dist/astryx/utils/interactionOverlay.stylex.js +1 -0
- package/dist/components/Board/Board.d.ts +102 -0
- package/dist/components/Board/Board.messages.d.ts +62 -0
- package/dist/components/Board/Board.messages.js +68 -0
- package/dist/components/Board/Board.messages.js.map +1 -0
- package/dist/components/Board/Board2.js +607 -0
- package/dist/components/Board/Board2.js.map +1 -0
- package/dist/components/Board/engine.d.ts +60 -0
- package/dist/components/Board/engine.js +397 -0
- package/dist/components/Board/engine.js.map +1 -0
- package/dist/components/Board/index.d.ts +4 -0
- package/dist/components/Board/index.js +3 -0
- package/dist/components/Board/layout.d.ts +81 -0
- package/dist/components/Board/layout.js +67 -0
- package/dist/components/Board/layout.js.map +1 -0
- package/dist/components/Board/scroll.d.ts +38 -0
- package/dist/components/Board/scroll.js +42 -0
- package/dist/components/Board/scroll.js.map +1 -0
- package/dist/components/DataGrid/DataGrid2.js +128 -128
- package/dist/components/DataGrid/DataGrid2.js.map +1 -1
- package/dist/components/Form/FormItem.d.ts +6 -1
- package/dist/components/Form/FormItem.js +68 -67
- package/dist/components/Form/FormItem.js.map +1 -1
- package/dist/components/Form/FormItemVisual.d.ts +7 -0
- package/dist/components/Form/FormItemVisual2.js +60 -55
- package/dist/components/Form/FormItemVisual2.js.map +1 -1
- package/dist/components/NotificationStack/NotificationStack2.js +1 -0
- package/dist/components/NotificationStack/NotificationStack2.js.map +1 -1
- package/dist/components/PageHeader/PageHeader2.js +4 -4
- package/dist/components/PagedSelector/PagedSelector.d.ts +1 -1
- package/dist/components/PagedSelector/PagedSelector2.js +37 -37
- package/dist/components/PagedSelector/PagedSelector2.js.map +1 -1
- package/dist/components/SelectionLabel/SelectionLabel.js +4 -4
- package/dist/forks/ComplexSelector/ComplexSelector.d.ts +82 -6
- package/dist/forks/ComplexSelector/ComplexSelector.js +170 -114
- package/dist/forks/ComplexSelector/ComplexSelector.js.map +1 -1
- package/dist/forks/ComplexSelector/ComplexSelector.styles.d.ts +40 -0
- package/dist/forks/ComplexSelector/ComplexSelector.styles.js +46 -6
- package/dist/forks/ComplexSelector/ComplexSelector.styles.js.map +1 -1
- package/dist/forks/ComplexSelector/index.d.ts +1 -1
- package/dist/forks/Drawer/Drawer.d.ts +21 -3
- package/dist/forks/Drawer/Drawer.js +70 -65
- package/dist/forks/Drawer/Drawer.js.map +1 -1
- package/dist/forks/Drawer/Drawer.styles.d.ts +16 -0
- package/dist/forks/Drawer/Drawer.styles.js +16 -0
- package/dist/forks/Drawer/Drawer.styles.js.map +1 -1
- package/dist/forks/Drawer/useDrawerDialogPresence.js.map +1 -1
- package/dist/forks/Tour/Tour.js +38 -40
- package/dist/forks/Tour/Tour.js.map +1 -1
- package/dist/forks/Tour/TourContext.js.map +1 -1
- package/dist/forks/Tour/TourStep.js.map +1 -1
- package/dist/forks/Tour/TourStep.styles.js.map +1 -1
- package/dist/forks/Tour/useTour.js.map +1 -1
- package/dist/i18n/catalog.js +27 -25
- package/dist/i18n/catalog.js.map +1 -1
- package/dist/i18n/locales/de-DE.js +60 -0
- package/dist/i18n/locales/de-DE.js.map +1 -1
- package/dist/i18n/locales/el-GR.js +60 -0
- package/dist/i18n/locales/el-GR.js.map +1 -1
- package/dist/i18n/locales/es-ES.js +60 -0
- package/dist/i18n/locales/es-ES.js.map +1 -1
- package/dist/i18n/locales/fi-FI.js +60 -0
- package/dist/i18n/locales/fi-FI.js.map +1 -1
- package/dist/i18n/locales/fr-FR.js +60 -0
- package/dist/i18n/locales/fr-FR.js.map +1 -1
- package/dist/i18n/locales/id-ID.js +60 -0
- package/dist/i18n/locales/id-ID.js.map +1 -1
- package/dist/i18n/locales/it-IT.js +60 -0
- package/dist/i18n/locales/it-IT.js.map +1 -1
- package/dist/i18n/locales/ja-JP.js +60 -0
- package/dist/i18n/locales/ja-JP.js.map +1 -1
- package/dist/i18n/locales/ko-KR.js +60 -0
- package/dist/i18n/locales/ko-KR.js.map +1 -1
- package/dist/i18n/locales/mn-MN.js +60 -0
- package/dist/i18n/locales/mn-MN.js.map +1 -1
- package/dist/i18n/locales/ms-MY.js +60 -0
- package/dist/i18n/locales/ms-MY.js.map +1 -1
- package/dist/i18n/locales/pl-PL.js +60 -0
- package/dist/i18n/locales/pl-PL.js.map +1 -1
- package/dist/i18n/locales/pt-BR.js +60 -0
- package/dist/i18n/locales/pt-BR.js.map +1 -1
- package/dist/i18n/locales/pt-PT.js +60 -0
- package/dist/i18n/locales/pt-PT.js.map +1 -1
- package/dist/i18n/locales/ru-RU.js +60 -0
- package/dist/i18n/locales/ru-RU.js.map +1 -1
- package/dist/i18n/locales/th-TH.js +60 -0
- package/dist/i18n/locales/th-TH.js.map +1 -1
- package/dist/i18n/locales/tr-TR.js +60 -0
- package/dist/i18n/locales/tr-TR.js.map +1 -1
- package/dist/i18n/locales/vi-VN.js +60 -0
- package/dist/i18n/locales/vi-VN.js.map +1 -1
- package/dist/i18n/locales/zh-CN.js +60 -0
- package/dist/i18n/locales/zh-CN.js.map +1 -1
- package/dist/i18n/locales/zh-TW.js +60 -0
- package/dist/i18n/locales/zh-TW.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +61 -59
- package/dist/instance.js +2 -2
- package/dist/locales/en.generated.js +34 -0
- package/dist/locales/en.json +136 -0
- package/dist/locales/pseudo.generated.js +34 -0
- package/dist/locales/pseudo.json +102 -0
- package/dist/theme/lablup/built/lablup.d.ts +3 -3
- package/dist/theme/lablup/built/lablup.js +24 -5
- package/dist/theme/lablup/built/lablup.variants.d.ts +2 -2
- package/dist/theme/lablup/built/theme.css +70 -63
- package/dist/theme/lablup/built/theme.css.d.ts +9 -0
- package/dist/ui-common-locales/de-DE.json +60 -0
- package/dist/ui-common-locales/el-GR.json +60 -0
- package/dist/ui-common-locales/en.json +60 -0
- package/dist/ui-common-locales/es-ES.json +60 -0
- package/dist/ui-common-locales/fi-FI.json +60 -0
- package/dist/ui-common-locales/fr-FR.json +60 -0
- package/dist/ui-common-locales/id-ID.json +60 -0
- package/dist/ui-common-locales/it-IT.json +60 -0
- package/dist/ui-common-locales/ja-JP.json +60 -0
- package/dist/ui-common-locales/ko-KR.json +60 -0
- package/dist/ui-common-locales/mn-MN.json +60 -0
- package/dist/ui-common-locales/ms-MY.json +60 -0
- package/dist/ui-common-locales/pl-PL.json +60 -0
- package/dist/ui-common-locales/pt-BR.json +60 -0
- package/dist/ui-common-locales/pt-PT.json +60 -0
- package/dist/ui-common-locales/ru-RU.json +60 -0
- package/dist/ui-common-locales/th-TH.json +60 -0
- package/dist/ui-common-locales/tr-TR.json +60 -0
- package/dist/ui-common-locales/vi-VN.json +60 -0
- package/dist/ui-common-locales/zh-CN.json +60 -0
- package/dist/ui-common-locales/zh-TW.json +60 -0
- package/exports.customs.json +1 -0
- package/package.json +86 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormItem.js","names":[],"sources":["../../../src/components/Form/FormItem.tsx"],"sourcesContent":["/**\n * `Form.Item`: the visual shell (`FormItemVisual`) bound to a `Field`.\n *\n * Four behaviours are load-bearing:\n *\n * 1. `required` is not derived from `rules` naively. Function rules\n * (`({ getFieldValue }) => ({ required: … })`) are evaluated against the\n * live form to decide whether the marker shows; a `warningOnly` required\n * rule does not show it.\n * 2. Layout-only items aggregate their children's errors. An item with no\n * `name` wrapping `noStyle` children collects their metas through\n * `NoStyleItemContext`; without it their messages disappear.\n * 3. `help={false}` opts out of bubbling, so a nested item that renders its\n * own help does not report into an ancestor as well.\n * 4. `noStyle` is a pure passthrough: state only, no shell.\n */\nimport Field, { type FieldProps } from \"./Field\";\nimport FormItemVisual from \"./FormItemVisual\";\nimport {\n FieldContext,\n FormConfigContext,\n FormItemInputContext,\n FormItemLayoutContext,\n ListContext,\n NoStyleItemContext,\n type FormItemCol,\n type FormItemStatusContextValue,\n type FormLayout,\n type RequiredMark,\n} from \"./context\";\nimport type {\n InternalFormInstance,\n Meta,\n Rule,\n RuleObject,\n StoreValue,\n} from \"./interface\";\nimport { toArray, type InternalNamePath, type NamePath } from \"./namePath\";\nimport * as React from \"react\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\n\nconst NAME_SPLIT = \"__SPLIT__\";\n\n/** antd's id scheme: `formName_a_b`, with a guard for a reserved DOM name. */\nconst ID_BLACKLIST = [\"parentNode\"];\nfunction getFieldId(namePath: InternalNamePath, formName?: string): string | undefined {\n if (!namePath.length) return undefined;\n const mergedId = namePath.join(\"_\");\n if (formName) return `${formName}_${mergedId}`;\n return ID_BLACKLIST.includes(mergedId) ? `form_item_${mergedId}` : mergedId;\n}\n\nfunction genEmptyMeta(): Meta {\n return {\n errors: [],\n warnings: [],\n touched: false,\n validating: false,\n name: [],\n validated: false,\n };\n}\n\nfunction getStatus(\n errors: React.ReactNode[],\n warnings: React.ReactNode[],\n meta: Meta,\n hasFeedback?: boolean,\n validateStatus?: FormItemStatusContextValue[\"status\"],\n): FormItemStatusContextValue[\"status\"] {\n if (validateStatus !== undefined) return validateStatus;\n if (meta.validating) return \"validating\";\n if (errors.length) return \"error\";\n if (warnings.length) return \"warning\";\n if (meta.touched || (hasFeedback && meta.validated)) return \"success\";\n return \"\";\n}\n\nexport interface FormItemProps<Values = any> extends Omit<\n FieldProps,\n \"children\" | \"name\" | \"onMetaChange\" | \"isList\"\n> {\n name?: NamePath;\n label?: React.ReactNode;\n /**\n * A node, or antd's `{ title, icon, placement, ... }` object form. `title`\n * becomes the tooltip body and `icon` the trigger glyph, rendered behind a\n * hover/focus target after the label. The other keys describe antd's\n * overlay and are ignored.\n */\n tooltip?: React.ReactNode | FormItemTooltipConfig;\n extra?: React.ReactNode;\n /** `false` also suppresses meta bubbling to an ancestor item. */\n help?: React.ReactNode;\n /** Overrides the asterisk derived from `rules`. */\n required?: boolean;\n /** Render no wrapper; bubble this field's meta to the nearest ancestor item. */\n noStyle?: boolean;\n hidden?: boolean;\n layout?: FormLayout;\n className?: string;\n style?: React.CSSProperties;\n hasFeedback?: boolean;\n validateStatus?: FormItemStatusContextValue[\"status\"];\n colon?: boolean;\n labelAlign?: \"left\" | \"right\";\n labelCol?: FormItemCol;\n wrapperCol?: FormItemCol;\n labelWrap?: boolean;\n htmlFor?: string;\n id?: string;\n fieldKey?: React.Key | React.Key[];\n children?: React.ReactNode | ((form: InternalFormInstance) => React.ReactNode);\n __values?: Values;\n}\n\nexport interface FormItemTooltipConfig {\n title?: React.ReactNode;\n icon?: React.ReactNode;\n placement?: string;\n [key: string]: unknown;\n}\n\nconst isTooltipConfig = (\n tooltip: FormItemProps[\"tooltip\"],\n): tooltip is FormItemTooltipConfig =>\n !!tooltip &&\n typeof tooltip === \"object\" &&\n !React.isValidElement(tooltip) &&\n \"title\" in tooltip;\n\n/** The tooltip BODY — antd's `tooltip` node, or the config object's `title`. */\nfunction normalizeTooltip(tooltip: FormItemProps[\"tooltip\"]): React.ReactNode {\n return isTooltipConfig(tooltip) ? tooltip.title : (tooltip as React.ReactNode);\n}\n\n/** The tooltip TRIGGER glyph — antd's `tooltip.icon`, when supplied. */\nfunction tooltipIcon(tooltip: FormItemProps[\"tooltip\"]): React.ReactNode {\n return isTooltipConfig(tooltip) ? tooltip.icon : undefined;\n}\n\ntype SubMetaMap = Record<\n string,\n {\n errors?: React.ReactNode[];\n warnings?: React.ReactNode[];\n name?: InternalNamePath;\n }\n>;\n\nconst FormItem = <Values,>(props: FormItemProps<Values>) => {\n const {\n name,\n label,\n tooltip,\n extra,\n help,\n required,\n noStyle,\n hidden,\n layout: propsLayout,\n className,\n style,\n hasFeedback,\n validateStatus,\n children,\n rules,\n dependencies,\n shouldUpdate,\n messageVariables,\n trigger = \"onChange\",\n validateTrigger,\n colon,\n labelAlign,\n labelCol,\n wrapperCol,\n labelWrap,\n htmlFor,\n fieldKey: _fieldKey,\n id: _id,\n __values: _values,\n ...restFieldProps\n } = props;\n\n const fieldContext = React.useContext(FieldContext);\n const { optionalLabel } = React.useContext(FormConfigContext);\n const t = useUicTranslator();\n const {\n layout: formLayout,\n requiredMark,\n name: formName,\n disabled: formDisabled,\n size: formSize,\n colon: formColon,\n labelAlign: formLabelAlign,\n labelCol: formLabelCol,\n wrapperCol: formWrapperCol,\n labelWrap: formLabelWrap,\n } = React.useContext(FormItemLayoutContext);\n const notifyParentMetaChange = React.useContext(NoStyleItemContext);\n const listContext = React.useContext(ListContext);\n\n const layout = propsLayout || formLayout;\n const mergedLabelCol = labelCol ?? formLabelCol;\n const mergedWrapperColProp = wrapperCol ?? formWrapperCol;\n /**\n * antd's `FormItemInput`: a LABEL-LESS item in a form that declares a\n * `labelCol` gets that span as a wrapper OFFSET, so its control still lines\n * up with the labelled rows above it.\n */\n const mergedWrapperCol =\n label === null &&\n !labelCol &&\n !wrapperCol &&\n formLabelCol?.span !== undefined &&\n formLabelCol.span < 24\n ? { ...mergedWrapperColProp, offset: formLabelCol.span }\n : mergedWrapperColProp;\n const mergedValidateTrigger =\n validateTrigger !== undefined ? validateTrigger : fieldContext.validateTrigger;\n\n const [meta, setMeta] = React.useState<Meta>(genEmptyMeta);\n const [subFieldErrors, setSubFieldErrors] = React.useState<SubMetaMap>({});\n // A removed list row no longer resolves through the key manager, so the\n // path used to unpublish it has to be the one we published under.\n const fieldKeyPathRef = React.useRef<InternalNamePath | null>(null);\n\n const onMetaChange = (nextMeta: Meta & { destroy?: boolean }) => {\n const keyInfo = listContext?.getKey(nextMeta.name);\n setMeta(nextMeta.destroy ? genEmptyMeta() : nextMeta);\n\n if (noStyle && help !== false && notifyParentMetaChange) {\n let namePath: InternalNamePath = nextMeta.name;\n if (!nextMeta.destroy) {\n if (keyInfo !== undefined) {\n const [fieldKey, restPath] = keyInfo;\n namePath = [fieldKey as string | number, ...restPath];\n fieldKeyPathRef.current = namePath;\n }\n } else {\n namePath = fieldKeyPathRef.current || namePath;\n }\n notifyParentMetaChange(nextMeta, namePath as React.Key[]);\n }\n };\n\n const onSubItemMetaChange = (\n subMeta: SubMetaMap[string] & { name: InternalNamePath; destroy?: boolean },\n uniqueKeys: React.Key[],\n ) => {\n setSubFieldErrors((prev) => {\n const clone = { ...prev };\n // name: ['user', 1] + key: [4] => 'user__SPLIT__4' — keyed by ROW\n // IDENTITY, so removing a list row drops the right entry.\n const mergedNameKey = [...subMeta.name.slice(0, -1), ...uniqueKeys].join(\n NAME_SPLIT,\n );\n if (subMeta.destroy) {\n delete clone[mergedNameKey];\n } else {\n clone[mergedNameKey] = subMeta;\n }\n return clone;\n });\n };\n\n const mergedErrors: React.ReactNode[] = [\n ...meta.errors,\n ...Object.values(subFieldErrors).flatMap((sub) => sub.errors ?? []),\n ];\n const mergedWarnings: React.ReactNode[] = [\n ...meta.warnings,\n ...Object.values(subFieldErrors).flatMap((sub) => sub.warnings ?? []),\n ];\n\n const renderLayout = (\n baseChildren: React.ReactNode,\n fieldId?: string,\n isRequired?: boolean,\n fieldHandle?: string,\n ) => {\n const status: FormItemStatusContextValue = {\n status: getStatus(\n mergedErrors,\n mergedWarnings,\n meta,\n hasFeedback,\n validateStatus,\n ),\n errors: mergedErrors,\n warnings: mergedWarnings,\n hasFeedback,\n isFormItemInput: true,\n };\n\n // A `noStyle` item contributes state only. Its errors were already sent\n // upward through `notifyParentMetaChange`.\n if (noStyle && !hidden) {\n return (\n <FormItemInputContext.Provider value={status}>\n {baseChildren}\n </FormItemInputContext.Provider>\n );\n }\n\n // antd keeps `-required` on the label in every case and hides the GLYPH\n // for `false` / `'optional'` / a function mark, because the label itself\n // then carries the hint. Mirrored so `.uic-form-item__label--required`\n // stays a stable anchor while the asterisk obeys the same rule.\n const requiredMarkType =\n requiredMark === false\n ? (\"hidden\" as const)\n : requiredMark === \"optional\" || typeof requiredMark === \"function\"\n ? (\"optional\" as const)\n : undefined;\n // An explicit `required` wins over the rules-derived value, and it is the\n // ONLY source on a layout-only item (no `name`, so no rules to derive\n // from). `BulkEditFormItem` renders exactly that shape — it drops\n // `name` from the item and drives the marker by hand.\n const mergedRequired = required !== undefined ? required : !!isRequired;\n const mergedLabel = applyRequiredMark(\n label,\n mergedRequired,\n requiredMark,\n optionalLabel ?? t(\"uic.Form.optional\"),\n );\n\n return (\n <FormItemInputContext.Provider value={status}>\n <FormItemVisual\n label={mergedLabel}\n labelTitle={typeof label === \"string\" ? label : undefined}\n tooltip={normalizeTooltip(tooltip)}\n tooltipIcon={tooltipIcon(tooltip)}\n extra={extra}\n help={help}\n required={mergedRequired}\n requiredMarkType={requiredMarkType}\n layout={layout}\n size={formSize}\n colon={colon ?? formColon}\n labelAlign={labelAlign ?? formLabelAlign}\n labelCol={mergedLabelCol}\n wrapperCol={mergedWrapperCol}\n labelWrap={labelWrap ?? formLabelWrap}\n status={status.status}\n hasFeedback={hasFeedback}\n className={className}\n style={style}\n hidden={hidden}\n fieldId={fieldId}\n fieldHandle={fieldHandle}\n htmlFor={htmlFor}\n errors={mergedErrors}\n warnings={mergedWarnings}\n >\n <NoStyleItemContext.Provider value={onSubItemMetaChange}>\n {baseChildren}\n </NoStyleItemContext.Provider>\n </FormItemVisual>\n </FormItemInputContext.Provider>\n );\n };\n\n const hasName = name !== undefined && name !== null;\n const isRenderProps = typeof children === \"function\";\n\n // Pure layout item: no field is registered, but the shell still renders and\n // still aggregates the `noStyle` children nested inside it.\n if (!hasName && !isRenderProps && !dependencies) {\n return renderLayout(children as React.ReactNode);\n }\n\n // `${label}` in a validate message resolves to the item's label when it is\n // a plain string, else to its name — matching antd's `messageVariables`.\n let variables: Record<string, string> = {};\n if (typeof label === \"string\") {\n variables.label = label;\n } else if (name) {\n variables.label = String(name);\n }\n if (messageVariables) {\n variables = { ...variables, ...messageVariables };\n }\n\n return (\n <Field\n {...restFieldProps}\n name={name}\n rules={rules}\n dependencies={dependencies}\n shouldUpdate={shouldUpdate}\n messageVariables={variables}\n trigger={trigger}\n validateTrigger={mergedValidateTrigger}\n onMetaChange={onMetaChange}\n >\n {(control, renderMeta, context) => {\n const mergedName = toArray(name as any).length ? renderMeta.name : [];\n const fieldId = getFieldId(mergedName, formName);\n const isRequired =\n required !== undefined ? required : isRuleRequired(rules, context);\n\n let childNode: React.ReactNode = null;\n\n if (isRenderProps && (shouldUpdate || dependencies) && !hasName) {\n childNode = (children as (form: InternalFormInstance) => React.ReactNode)(\n context,\n );\n } else if (React.isValidElement(children)) {\n const element = children as React.ReactElement<Record<string, any>>;\n const childProps: Record<string, any> = {\n ...element.props,\n ...control,\n };\n if (!childProps.id) {\n childProps.id = fieldId;\n }\n // Astryx controls replace `id` with their own `useId()` but pass\n // `data-*` through; this is what `FormStore.getFieldDOMNode` finds.\n if (mergedName.length) {\n childProps[\"data-uic-field-id\"] = mergedName.join(\"_\");\n }\n if (formDisabled && childProps.disabled === undefined) {\n childProps.disabled = true;\n }\n\n const describedbyArr: string[] = [];\n if (help || mergedErrors.length > 0) {\n describedbyArr.push(`${fieldId}_help`);\n }\n if (extra) {\n describedbyArr.push(`${fieldId}_extra`);\n }\n if (describedbyArr.length) {\n childProps[\"aria-describedby\"] = describedbyArr.join(\" \");\n }\n if (mergedErrors.length > 0) {\n childProps[\"aria-invalid\"] = \"true\";\n }\n if (isRequired) {\n childProps[\"aria-required\"] = \"true\";\n }\n\n // Keep the child's own handlers: `control[trigger]` writes to the\n // store, then the author's handler runs.\n const triggers = new Set<string>([\n ...toArray(trigger),\n ...toArray(mergedValidateTrigger as string | string[]),\n ]);\n triggers.forEach((eventName) => {\n childProps[eventName] = (...args: any[]) => {\n control[eventName]?.(...args);\n (element.props as any)[eventName]?.(...args);\n };\n });\n\n childNode = React.cloneElement(element, childProps);\n } else {\n childNode = children as React.ReactNode;\n }\n\n return renderLayout(\n childNode,\n fieldId,\n isRequired,\n mergedName.length ? mergedName.join(\"_\") : undefined,\n );\n }}\n </Field>\n );\n};\n\n/**\n * Evaluate `rules` — INCLUDING function rules, against the live form — to\n * decide whether the required marker shows. A `warningOnly` required rule\n * does not count: it is advice, not a requirement.\n */\nfunction isRuleRequired(\n rules: Rule[] | undefined,\n form: InternalFormInstance,\n): boolean {\n return !!rules?.some((rule) => {\n if (typeof rule === \"function\") {\n const resolved = rule(form) as RuleObject | undefined;\n return !!resolved?.required && !resolved?.warningOnly;\n }\n return !!rule?.required && !rule?.warningOnly;\n });\n}\n\n/**\n * `requiredMark` decorates the LABEL. A function or `'optional'` mark replaces\n * the asterisk entirely, as in antd: the label itself then carries the hint.\n */\nfunction applyRequiredMark(\n label: React.ReactNode,\n required: boolean,\n requiredMark: RequiredMark | undefined,\n optionalLabel: React.ReactNode,\n): React.ReactNode {\n if (label === undefined || label === null) return label;\n if (typeof requiredMark === \"function\") {\n return requiredMark(label, { required });\n }\n if (requiredMark === \"optional\" && !required) {\n // antd renders the bare locale string and spaces it with\n // `margin-inline-start: marginXXS`, so the gap survives text selection and\n // copy. The engine had baked a literal leading space into the string,\n // which put \" (optional)\" in `label.textContent` where antd has\n // \"(optional)\".\n return (\n <>\n {label}\n <span className=\"uic-form-item__optional\">{optionalLabel}</span>\n </>\n );\n }\n return label;\n}\n\n/** Errors/warnings of the nearest enclosing `Form.Item`. */\nexport const useFormItemStatus = () => {\n const { status, errors = [], warnings = [] } = React.useContext(FormItemInputContext);\n return { status, errors, warnings };\n};\n\nexport { getFieldId };\nexport default FormItem;\nexport type { StoreValue };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormItem.js","names":[],"sources":["../../../src/components/Form/FormItem.tsx"],"sourcesContent":["/**\n * `Form.Item`: the visual shell (`FormItemVisual`) bound to a `Field`.\n *\n * Four behaviours are load-bearing:\n *\n * 1. `required` is not derived from `rules` naively. Function rules\n * (`({ getFieldValue }) => ({ required: … })`) are evaluated against the\n * live form to decide whether the marker shows; a `warningOnly` required\n * rule does not show it.\n * 2. Layout-only items aggregate their children's errors. An item with no\n * `name` wrapping `noStyle` children collects their metas through\n * `NoStyleItemContext`; without it their messages disappear.\n * 3. `help={false}` opts out of bubbling, so a nested item that renders its\n * own help does not report into an ancestor as well.\n * 4. `noStyle` is a pure passthrough: state only, no shell.\n */\nimport Field, { type FieldProps } from \"./Field\";\nimport FormItemVisual from \"./FormItemVisual\";\nimport {\n FieldContext,\n FormConfigContext,\n FormItemInputContext,\n FormItemLayoutContext,\n ListContext,\n NoStyleItemContext,\n type FormItemCol,\n type FormItemStatusContextValue,\n type FormLayout,\n type RequiredMark,\n} from \"./context\";\nimport type {\n InternalFormInstance,\n Meta,\n Rule,\n RuleObject,\n StoreValue,\n} from \"./interface\";\nimport { toArray, type InternalNamePath, type NamePath } from \"./namePath\";\nimport * as React from \"react\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\n\nconst NAME_SPLIT = \"__SPLIT__\";\n\n/** antd's id scheme: `formName_a_b`, with a guard for a reserved DOM name. */\nconst ID_BLACKLIST = [\"parentNode\"];\nfunction getFieldId(namePath: InternalNamePath, formName?: string): string | undefined {\n if (!namePath.length) return undefined;\n const mergedId = namePath.join(\"_\");\n if (formName) return `${formName}_${mergedId}`;\n return ID_BLACKLIST.includes(mergedId) ? `form_item_${mergedId}` : mergedId;\n}\n\nfunction genEmptyMeta(): Meta {\n return {\n errors: [],\n warnings: [],\n touched: false,\n validating: false,\n name: [],\n validated: false,\n };\n}\n\nfunction getStatus(\n errors: React.ReactNode[],\n warnings: React.ReactNode[],\n meta: Meta,\n hasFeedback?: boolean,\n validateStatus?: FormItemStatusContextValue[\"status\"],\n): FormItemStatusContextValue[\"status\"] {\n if (validateStatus !== undefined) return validateStatus;\n if (meta.validating) return \"validating\";\n if (errors.length) return \"error\";\n if (warnings.length) return \"warning\";\n if (meta.touched || (hasFeedback && meta.validated)) return \"success\";\n return \"\";\n}\n\nexport interface FormItemProps<Values = any> extends Omit<\n FieldProps,\n \"children\" | \"name\" | \"onMetaChange\" | \"isList\"\n> {\n name?: NamePath;\n label?: React.ReactNode;\n /**\n * A node, or antd's `{ title, icon, placement, ... }` object form. `title`\n * becomes the tooltip body and `icon` the trigger glyph, rendered behind a\n * hover/focus target after the label. The other keys describe antd's\n * overlay and are ignored.\n */\n tooltip?: React.ReactNode | FormItemTooltipConfig;\n /**\n * Actions for the label row: far end in vertical layout, right after the\n * label in horizontal / inline layout. Needs a `label`.\n */\n labelExtra?: React.ReactNode;\n extra?: React.ReactNode;\n /** `false` also suppresses meta bubbling to an ancestor item. */\n help?: React.ReactNode;\n /** Overrides the asterisk derived from `rules`. */\n required?: boolean;\n /** Render no wrapper; bubble this field's meta to the nearest ancestor item. */\n noStyle?: boolean;\n hidden?: boolean;\n layout?: FormLayout;\n className?: string;\n style?: React.CSSProperties;\n hasFeedback?: boolean;\n validateStatus?: FormItemStatusContextValue[\"status\"];\n colon?: boolean;\n labelAlign?: \"left\" | \"right\";\n labelCol?: FormItemCol;\n wrapperCol?: FormItemCol;\n labelWrap?: boolean;\n htmlFor?: string;\n id?: string;\n fieldKey?: React.Key | React.Key[];\n children?: React.ReactNode | ((form: InternalFormInstance) => React.ReactNode);\n __values?: Values;\n}\n\nexport interface FormItemTooltipConfig {\n title?: React.ReactNode;\n icon?: React.ReactNode;\n placement?: string;\n [key: string]: unknown;\n}\n\nconst isTooltipConfig = (\n tooltip: FormItemProps[\"tooltip\"],\n): tooltip is FormItemTooltipConfig =>\n !!tooltip &&\n typeof tooltip === \"object\" &&\n !React.isValidElement(tooltip) &&\n \"title\" in tooltip;\n\n/** The tooltip BODY — antd's `tooltip` node, or the config object's `title`. */\nfunction normalizeTooltip(tooltip: FormItemProps[\"tooltip\"]): React.ReactNode {\n return isTooltipConfig(tooltip) ? tooltip.title : (tooltip as React.ReactNode);\n}\n\n/** The tooltip TRIGGER glyph — antd's `tooltip.icon`, when supplied. */\nfunction tooltipIcon(tooltip: FormItemProps[\"tooltip\"]): React.ReactNode {\n return isTooltipConfig(tooltip) ? tooltip.icon : undefined;\n}\n\ntype SubMetaMap = Record<\n string,\n {\n errors?: React.ReactNode[];\n warnings?: React.ReactNode[];\n name?: InternalNamePath;\n }\n>;\n\nconst FormItem = <Values,>(props: FormItemProps<Values>) => {\n const {\n name,\n label,\n tooltip,\n labelExtra,\n extra,\n help,\n required,\n noStyle,\n hidden,\n layout: propsLayout,\n className,\n style,\n hasFeedback,\n validateStatus,\n children,\n rules,\n dependencies,\n shouldUpdate,\n messageVariables,\n trigger = \"onChange\",\n validateTrigger,\n colon,\n labelAlign,\n labelCol,\n wrapperCol,\n labelWrap,\n htmlFor,\n fieldKey: _fieldKey,\n id: _id,\n __values: _values,\n ...restFieldProps\n } = props;\n\n const fieldContext = React.useContext(FieldContext);\n const { optionalLabel } = React.useContext(FormConfigContext);\n const t = useUicTranslator();\n const {\n layout: formLayout,\n requiredMark,\n name: formName,\n disabled: formDisabled,\n size: formSize,\n colon: formColon,\n labelAlign: formLabelAlign,\n labelCol: formLabelCol,\n wrapperCol: formWrapperCol,\n labelWrap: formLabelWrap,\n } = React.useContext(FormItemLayoutContext);\n const notifyParentMetaChange = React.useContext(NoStyleItemContext);\n const listContext = React.useContext(ListContext);\n\n const layout = propsLayout || formLayout;\n const mergedLabelCol = labelCol ?? formLabelCol;\n const mergedWrapperColProp = wrapperCol ?? formWrapperCol;\n /**\n * antd's `FormItemInput`: a LABEL-LESS item in a form that declares a\n * `labelCol` gets that span as a wrapper OFFSET, so its control still lines\n * up with the labelled rows above it.\n */\n const mergedWrapperCol =\n label === null &&\n !labelCol &&\n !wrapperCol &&\n formLabelCol?.span !== undefined &&\n formLabelCol.span < 24\n ? { ...mergedWrapperColProp, offset: formLabelCol.span }\n : mergedWrapperColProp;\n const mergedValidateTrigger =\n validateTrigger !== undefined ? validateTrigger : fieldContext.validateTrigger;\n\n const [meta, setMeta] = React.useState<Meta>(genEmptyMeta);\n const [subFieldErrors, setSubFieldErrors] = React.useState<SubMetaMap>({});\n // A removed list row no longer resolves through the key manager, so the\n // path used to unpublish it has to be the one we published under.\n const fieldKeyPathRef = React.useRef<InternalNamePath | null>(null);\n\n const onMetaChange = (nextMeta: Meta & { destroy?: boolean }) => {\n const keyInfo = listContext?.getKey(nextMeta.name);\n setMeta(nextMeta.destroy ? genEmptyMeta() : nextMeta);\n\n if (noStyle && help !== false && notifyParentMetaChange) {\n let namePath: InternalNamePath = nextMeta.name;\n if (!nextMeta.destroy) {\n if (keyInfo !== undefined) {\n const [fieldKey, restPath] = keyInfo;\n namePath = [fieldKey as string | number, ...restPath];\n fieldKeyPathRef.current = namePath;\n }\n } else {\n namePath = fieldKeyPathRef.current || namePath;\n }\n notifyParentMetaChange(nextMeta, namePath as React.Key[]);\n }\n };\n\n const onSubItemMetaChange = (\n subMeta: SubMetaMap[string] & { name: InternalNamePath; destroy?: boolean },\n uniqueKeys: React.Key[],\n ) => {\n setSubFieldErrors((prev) => {\n const clone = { ...prev };\n // name: ['user', 1] + key: [4] => 'user__SPLIT__4' — keyed by ROW\n // IDENTITY, so removing a list row drops the right entry.\n const mergedNameKey = [...subMeta.name.slice(0, -1), ...uniqueKeys].join(\n NAME_SPLIT,\n );\n if (subMeta.destroy) {\n delete clone[mergedNameKey];\n } else {\n clone[mergedNameKey] = subMeta;\n }\n return clone;\n });\n };\n\n const mergedErrors: React.ReactNode[] = [\n ...meta.errors,\n ...Object.values(subFieldErrors).flatMap((sub) => sub.errors ?? []),\n ];\n const mergedWarnings: React.ReactNode[] = [\n ...meta.warnings,\n ...Object.values(subFieldErrors).flatMap((sub) => sub.warnings ?? []),\n ];\n\n const renderLayout = (\n baseChildren: React.ReactNode,\n fieldId?: string,\n isRequired?: boolean,\n fieldHandle?: string,\n ) => {\n const status: FormItemStatusContextValue = {\n status: getStatus(\n mergedErrors,\n mergedWarnings,\n meta,\n hasFeedback,\n validateStatus,\n ),\n errors: mergedErrors,\n warnings: mergedWarnings,\n hasFeedback,\n isFormItemInput: true,\n };\n\n // A `noStyle` item contributes state only. Its errors were already sent\n // upward through `notifyParentMetaChange`.\n if (noStyle && !hidden) {\n return (\n <FormItemInputContext.Provider value={status}>\n {baseChildren}\n </FormItemInputContext.Provider>\n );\n }\n\n // antd keeps `-required` on the label in every case and hides the GLYPH\n // for `false` / `'optional'` / a function mark, because the label itself\n // then carries the hint. Mirrored so `.uic-form-item__label--required`\n // stays a stable anchor while the asterisk obeys the same rule.\n const requiredMarkType =\n requiredMark === false\n ? (\"hidden\" as const)\n : requiredMark === \"optional\" || typeof requiredMark === \"function\"\n ? (\"optional\" as const)\n : undefined;\n // An explicit `required` wins over the rules-derived value, and it is the\n // ONLY source on a layout-only item (no `name`, so no rules to derive\n // from). `BulkEditFormItem` renders exactly that shape — it drops\n // `name` from the item and drives the marker by hand.\n const mergedRequired = required !== undefined ? required : !!isRequired;\n const mergedLabel = applyRequiredMark(\n label,\n mergedRequired,\n requiredMark,\n optionalLabel ?? t(\"uic.Form.optional\"),\n );\n\n return (\n <FormItemInputContext.Provider value={status}>\n <FormItemVisual\n label={mergedLabel}\n labelTitle={typeof label === \"string\" ? label : undefined}\n tooltip={normalizeTooltip(tooltip)}\n tooltipIcon={tooltipIcon(tooltip)}\n labelExtra={labelExtra}\n extra={extra}\n help={help}\n required={mergedRequired}\n requiredMarkType={requiredMarkType}\n layout={layout}\n size={formSize}\n colon={colon ?? formColon}\n labelAlign={labelAlign ?? formLabelAlign}\n labelCol={mergedLabelCol}\n wrapperCol={mergedWrapperCol}\n labelWrap={labelWrap ?? formLabelWrap}\n status={status.status}\n hasFeedback={hasFeedback}\n className={className}\n style={style}\n hidden={hidden}\n fieldId={fieldId}\n fieldHandle={fieldHandle}\n htmlFor={htmlFor}\n errors={mergedErrors}\n warnings={mergedWarnings}\n >\n <NoStyleItemContext.Provider value={onSubItemMetaChange}>\n {baseChildren}\n </NoStyleItemContext.Provider>\n </FormItemVisual>\n </FormItemInputContext.Provider>\n );\n };\n\n const hasName = name !== undefined && name !== null;\n const isRenderProps = typeof children === \"function\";\n\n // Pure layout item: no field is registered, but the shell still renders and\n // still aggregates the `noStyle` children nested inside it.\n if (!hasName && !isRenderProps && !dependencies) {\n return renderLayout(children as React.ReactNode);\n }\n\n // `${label}` in a validate message resolves to the item's label when it is\n // a plain string, else to its name — matching antd's `messageVariables`.\n let variables: Record<string, string> = {};\n if (typeof label === \"string\") {\n variables.label = label;\n } else if (name) {\n variables.label = String(name);\n }\n if (messageVariables) {\n variables = { ...variables, ...messageVariables };\n }\n\n return (\n <Field\n {...restFieldProps}\n name={name}\n rules={rules}\n dependencies={dependencies}\n shouldUpdate={shouldUpdate}\n messageVariables={variables}\n trigger={trigger}\n validateTrigger={mergedValidateTrigger}\n onMetaChange={onMetaChange}\n >\n {(control, renderMeta, context) => {\n const mergedName = toArray(name as any).length ? renderMeta.name : [];\n const fieldId = getFieldId(mergedName, formName);\n const isRequired =\n required !== undefined ? required : isRuleRequired(rules, context);\n\n let childNode: React.ReactNode = null;\n\n if (isRenderProps && (shouldUpdate || dependencies) && !hasName) {\n childNode = (children as (form: InternalFormInstance) => React.ReactNode)(\n context,\n );\n } else if (React.isValidElement(children)) {\n const element = children as React.ReactElement<Record<string, any>>;\n const childProps: Record<string, any> = {\n ...element.props,\n ...control,\n };\n if (!childProps.id) {\n childProps.id = fieldId;\n }\n // Astryx controls replace `id` with their own `useId()` but pass\n // `data-*` through; this is what `FormStore.getFieldDOMNode` finds.\n if (mergedName.length) {\n childProps[\"data-uic-field-id\"] = mergedName.join(\"_\");\n }\n if (formDisabled && childProps.disabled === undefined) {\n childProps.disabled = true;\n }\n\n const describedbyArr: string[] = [];\n if (help || mergedErrors.length > 0) {\n describedbyArr.push(`${fieldId}_help`);\n }\n if (extra) {\n describedbyArr.push(`${fieldId}_extra`);\n }\n if (describedbyArr.length) {\n childProps[\"aria-describedby\"] = describedbyArr.join(\" \");\n }\n if (mergedErrors.length > 0) {\n childProps[\"aria-invalid\"] = \"true\";\n }\n if (isRequired) {\n childProps[\"aria-required\"] = \"true\";\n }\n\n // Keep the child's own handlers: `control[trigger]` writes to the\n // store, then the author's handler runs.\n const triggers = new Set<string>([\n ...toArray(trigger),\n ...toArray(mergedValidateTrigger as string | string[]),\n ]);\n triggers.forEach((eventName) => {\n childProps[eventName] = (...args: any[]) => {\n control[eventName]?.(...args);\n (element.props as any)[eventName]?.(...args);\n };\n });\n\n childNode = React.cloneElement(element, childProps);\n } else {\n childNode = children as React.ReactNode;\n }\n\n return renderLayout(\n childNode,\n fieldId,\n isRequired,\n mergedName.length ? mergedName.join(\"_\") : undefined,\n );\n }}\n </Field>\n );\n};\n\n/**\n * Evaluate `rules` — INCLUDING function rules, against the live form — to\n * decide whether the required marker shows. A `warningOnly` required rule\n * does not count: it is advice, not a requirement.\n */\nfunction isRuleRequired(\n rules: Rule[] | undefined,\n form: InternalFormInstance,\n): boolean {\n return !!rules?.some((rule) => {\n if (typeof rule === \"function\") {\n const resolved = rule(form) as RuleObject | undefined;\n return !!resolved?.required && !resolved?.warningOnly;\n }\n return !!rule?.required && !rule?.warningOnly;\n });\n}\n\n/**\n * `requiredMark` decorates the LABEL. A function or `'optional'` mark replaces\n * the asterisk entirely, as in antd: the label itself then carries the hint.\n */\nfunction applyRequiredMark(\n label: React.ReactNode,\n required: boolean,\n requiredMark: RequiredMark | undefined,\n optionalLabel: React.ReactNode,\n): React.ReactNode {\n if (label === undefined || label === null) return label;\n if (typeof requiredMark === \"function\") {\n return requiredMark(label, { required });\n }\n if (requiredMark === \"optional\" && !required) {\n // antd renders the bare locale string and spaces it with\n // `margin-inline-start: marginXXS`, so the gap survives text selection and\n // copy. The engine had baked a literal leading space into the string,\n // which put \" (optional)\" in `label.textContent` where antd has\n // \"(optional)\".\n return (\n <>\n {label}\n <span className=\"uic-form-item__optional\">{optionalLabel}</span>\n </>\n );\n }\n return label;\n}\n\n/** Errors/warnings of the nearest enclosing `Form.Item`. */\nexport const useFormItemStatus = () => {\n const { status, errors = [], warnings = [] } = React.useContext(FormItemInputContext);\n return { status, errors, warnings };\n};\n\nexport { getFieldId };\nexport default FormItem;\nexport type { StoreValue };\n"],"mappings":"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"}
|
|
@@ -22,6 +22,13 @@ export interface FormItemVisualProps {
|
|
|
22
22
|
tooltip?: React.ReactNode;
|
|
23
23
|
/** The tooltip trigger glyph. Defaults to a question mark in a circle. */
|
|
24
24
|
tooltipIcon?: React.ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Actions that belong to the label row (e.g. a filter button). Rendered
|
|
27
|
+
* outside the `<label>`, so they stay out of the control's accessible name:
|
|
28
|
+
* at the far end of the row in vertical layout, right after the label
|
|
29
|
+
* otherwise. Needs a `label`.
|
|
30
|
+
*/
|
|
31
|
+
labelExtra?: React.ReactNode;
|
|
25
32
|
extra?: React.ReactNode;
|
|
26
33
|
help?: React.ReactNode;
|
|
27
34
|
/** Renders the required marker. Independent of the `required` rule. */
|
|
@@ -13,23 +13,24 @@ var o = (...e) => e.filter(Boolean).join(" "), s = 24, c = (e, t) => {
|
|
|
13
13
|
n.maxWidth = r, t === "label" && (n.flex = `0 0 ${r}`);
|
|
14
14
|
}
|
|
15
15
|
return e.flex !== void 0 && (n.flex = typeof e.flex == "number" ? `${e.flex} ${e.flex} auto` : e.flex), e.offset !== void 0 && (n.marginInlineStart = `${e.offset / s * 100}%`), n;
|
|
16
|
-
}, l = ({ label: s, labelTitle: l, tooltip: u, tooltipIcon: d,
|
|
17
|
-
let
|
|
16
|
+
}, l = ({ label: s, labelTitle: l, tooltip: u, tooltipIcon: d, labelExtra: f, extra: p, help: m, required: h, requiredMarkType: g, layout: _ = "vertical", size: v, colon: y, labelAlign: b, labelCol: x, wrapperCol: S, labelWrap: C, errors: w = [], warnings: T = [], status: E, hasFeedback: D, fieldId: O, fieldHandle: k, htmlFor: A, className: j, style: M, hidden: N, children: P }) => {
|
|
17
|
+
let F = m != null && m !== !1, I = F || w.length > 0 || T.length > 0, L = t.useRef(null), R = t.useRef(null), [z, B] = t.useState(null), [V, H] = t.useState(0);
|
|
18
18
|
t.useLayoutEffect(() => {
|
|
19
|
-
|
|
20
|
-
}, [
|
|
21
|
-
|
|
22
|
-
}, [
|
|
23
|
-
let
|
|
24
|
-
className: o("uic-form-item__label-col",
|
|
25
|
-
"data-align":
|
|
26
|
-
"data-wrap":
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"data-
|
|
19
|
+
I && L.current ? B(Number.parseInt(getComputedStyle(L.current).marginBottom, 10) || 0) : B(null);
|
|
20
|
+
}, [I]), t.useLayoutEffect(() => {
|
|
21
|
+
H(p && R.current ? R.current.clientHeight : 0);
|
|
22
|
+
}, [p]);
|
|
23
|
+
let U = f != null && f !== !1, W = s == null ? null : /* @__PURE__ */ r("div", {
|
|
24
|
+
className: o("uic-form-item__label-col", x?.className),
|
|
25
|
+
"data-align": b === "left" ? "left" : void 0,
|
|
26
|
+
"data-wrap": C ? "" : void 0,
|
|
27
|
+
"data-has-label-extra": U ? "" : void 0,
|
|
28
|
+
style: c(x, "label"),
|
|
29
|
+
children: [/* @__PURE__ */ r("label", {
|
|
30
|
+
htmlFor: A ?? O,
|
|
31
|
+
className: o("uic-form-item__label", h && "uic-form-item__label--required"),
|
|
32
|
+
"data-required-mark": g,
|
|
33
|
+
"data-no-colon": y === !1 ? "" : void 0,
|
|
33
34
|
title: l ?? (typeof s == "string" ? s : void 0),
|
|
34
35
|
children: [s, u ? /* @__PURE__ */ n(i, {
|
|
35
36
|
content: u,
|
|
@@ -42,71 +43,75 @@ var o = (...e) => e.filter(Boolean).join(" "), s = 24, c = (e, t) => {
|
|
|
42
43
|
children: d ?? /* @__PURE__ */ n(a, { size: "1em" })
|
|
43
44
|
})
|
|
44
45
|
}) : null]
|
|
45
|
-
})
|
|
46
|
-
|
|
46
|
+
}), U ? /* @__PURE__ */ n("span", {
|
|
47
|
+
className: "uic-form-item__label-extra",
|
|
48
|
+
"data-no-colon": y === !1 ? "" : void 0,
|
|
49
|
+
children: f
|
|
50
|
+
}) : null]
|
|
51
|
+
}), G = F ? [{
|
|
47
52
|
key: "help",
|
|
48
|
-
node:
|
|
49
|
-
kind:
|
|
50
|
-
}] : [...
|
|
53
|
+
node: m,
|
|
54
|
+
kind: E === "error" ? "error" : E === "warning" ? "warning" : null
|
|
55
|
+
}] : [...w.map((e, t) => ({
|
|
51
56
|
key: `e-${t}`,
|
|
52
57
|
node: e,
|
|
53
58
|
kind: "error"
|
|
54
|
-
})), ...
|
|
59
|
+
})), ...T.map((e, t) => ({
|
|
55
60
|
key: `w-${t}`,
|
|
56
61
|
node: e,
|
|
57
62
|
kind: "warning"
|
|
58
|
-
}))],
|
|
59
|
-
id:
|
|
63
|
+
}))], K = G.length ? /* @__PURE__ */ n("div", {
|
|
64
|
+
id: O ? `${O}_help` : void 0,
|
|
60
65
|
className: "uic-form-item__explain",
|
|
61
|
-
children:
|
|
66
|
+
children: G.map((e) => /* @__PURE__ */ n("div", {
|
|
62
67
|
className: e.kind ? `uic-form-item__explain-${e.kind}` : void 0,
|
|
63
68
|
children: e.node
|
|
64
69
|
}, e.key))
|
|
65
|
-
}) : null,
|
|
66
|
-
ref:
|
|
67
|
-
id:
|
|
70
|
+
}) : null, q = p ? /* @__PURE__ */ n("div", {
|
|
71
|
+
ref: R,
|
|
72
|
+
id: O ? `${O}_extra` : void 0,
|
|
68
73
|
className: "uic-form-item__extra",
|
|
69
|
-
children:
|
|
70
|
-
}) : null,
|
|
74
|
+
children: p
|
|
75
|
+
}) : null, J = D && E ? /* @__PURE__ */ n("span", {
|
|
71
76
|
className: "uic-form-item__feedback-icon",
|
|
72
|
-
"data-status":
|
|
73
|
-
children: e(
|
|
74
|
-
}) : null,
|
|
75
|
-
className: o("uic-form-item__control",
|
|
76
|
-
"data-span":
|
|
77
|
-
style: c(
|
|
77
|
+
"data-status": E,
|
|
78
|
+
children: e(E)
|
|
79
|
+
}) : null, Y = /* @__PURE__ */ r("div", {
|
|
80
|
+
className: o("uic-form-item__control", S?.className),
|
|
81
|
+
"data-span": S?.span === void 0 ? void 0 : "",
|
|
82
|
+
style: c(S, "control"),
|
|
78
83
|
children: [/* @__PURE__ */ r("div", {
|
|
79
84
|
className: "uic-form-item__control-input",
|
|
80
85
|
children: [/* @__PURE__ */ n("div", {
|
|
81
86
|
className: "uic-form-item__control-input-content",
|
|
82
|
-
children:
|
|
83
|
-
}),
|
|
84
|
-
}),
|
|
87
|
+
children: P
|
|
88
|
+
}), J]
|
|
89
|
+
}), K || q ? /* @__PURE__ */ r("div", {
|
|
85
90
|
className: "uic-form-item__additional",
|
|
86
|
-
style:
|
|
87
|
-
children: [
|
|
91
|
+
style: z ? { minHeight: z + V } : void 0,
|
|
92
|
+
children: [K, q]
|
|
88
93
|
}) : null]
|
|
89
94
|
});
|
|
90
95
|
return /* @__PURE__ */ r("div", {
|
|
91
|
-
ref:
|
|
92
|
-
className: o("uic-form-item",
|
|
93
|
-
"data-uic-field-item":
|
|
94
|
-
"data-layout":
|
|
95
|
-
"data-size":
|
|
96
|
-
"data-status":
|
|
97
|
-
"data-feedback":
|
|
98
|
-
"data-hidden":
|
|
99
|
-
style:
|
|
96
|
+
ref: L,
|
|
97
|
+
className: o("uic-form-item", j),
|
|
98
|
+
"data-uic-field-item": k,
|
|
99
|
+
"data-layout": _,
|
|
100
|
+
"data-size": v,
|
|
101
|
+
"data-status": E || void 0,
|
|
102
|
+
"data-feedback": J ? "" : void 0,
|
|
103
|
+
"data-hidden": N ? "" : void 0,
|
|
104
|
+
style: N ? {
|
|
100
105
|
display: "none",
|
|
101
|
-
...
|
|
102
|
-
} :
|
|
106
|
+
...M
|
|
107
|
+
} : M,
|
|
103
108
|
children: [/* @__PURE__ */ r("div", {
|
|
104
109
|
className: "uic-form-item__row",
|
|
105
|
-
children: [
|
|
106
|
-
}),
|
|
110
|
+
children: [W, Y]
|
|
111
|
+
}), z ? /* @__PURE__ */ n("div", {
|
|
107
112
|
"aria-hidden": !0,
|
|
108
113
|
className: "uic-form-item__margin-offset",
|
|
109
|
-
style: { marginBottom: -
|
|
114
|
+
style: { marginBottom: -z }
|
|
110
115
|
}) : null]
|
|
111
116
|
});
|
|
112
117
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormItemVisual2.js","names":[],"sources":["../../../src/components/Form/FormItemVisual.tsx"],"sourcesContent":["/**\n * The presentational shell of a form item: label, control, explain and extra,\n * laid out the way antd lays out `Form.Item` (`layout`, `labelCol` /\n * `wrapperCol` spans, `colon`, `labelAlign`, `labelWrap`, `size`,\n * `hasFeedback`, `validateStatus`), so a form moved onto the engine keeps its\n * geometry. Styles are in `FormItemVisual.css`.\n *\n * .uic-form-item\n * .uic-form-item__row\n * .uic-form-item__label-col\n * label.uic-form-item__label (+ .uic-form-item__tooltip)\n * .uic-form-item__control\n * .uic-form-item__control-input\n * .uic-form-item__control-input-content\n * .uic-form-item__feedback-icon\n * .uic-form-item__additional\n * .uic-form-item__explain (.uic-form-item__explain-error / -warning rows)\n * .uic-form-item__extra\n * .uic-form-item__margin-offset\n */\nimport { CircleHelp } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Tooltip } from \"@astryxdesign/core/Tooltip\";\n\nimport getFeedbackIcon from \"./feedbackIcons\";\nimport \"./FormItemVisual.css\";\n\nexport type FormItemLayout = \"vertical\" | \"horizontal\" | \"inline\";\nexport type FormItemSize = \"small\" | \"middle\" | \"large\";\nexport type FormItemStatus = \"success\" | \"warning\" | \"error\" | \"validating\" | \"\";\n\n/** antd's `Col` props, reduced to `span`, `offset`, `flex`, `className` and `style`. */\nexport interface FormItemCol {\n span?: number;\n offset?: number;\n flex?: string | number;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface FormItemVisualProps {\n label?: React.ReactNode;\n /**\n * `<label title>`. Separate from `label` because it is taken from the\n * original prop, before `requiredMark` wraps it: a function mark turns the\n * label into an element and the title would disappear.\n */\n labelTitle?: string;\n /** The tooltip body, rendered behind a help glyph after the label. */\n tooltip?: React.ReactNode;\n /** The tooltip trigger glyph. Defaults to a question mark in a circle. */\n tooltipIcon?: React.ReactNode;\n extra?: React.ReactNode;\n help?: React.ReactNode;\n /** Renders the required marker. Independent of the `required` rule. */\n required?: boolean;\n /**\n * How the marker is drawn: `'optional'` and `'hidden'` hide the asterisk (a\n * function or `'optional'` `requiredMark` puts the hint in the label).\n */\n requiredMarkType?: \"optional\" | \"hidden\";\n layout?: FormItemLayout;\n size?: FormItemSize;\n colon?: boolean;\n labelAlign?: \"left\" | \"right\";\n labelCol?: FormItemCol;\n wrapperCol?: FormItemCol;\n labelWrap?: boolean;\n errors?: React.ReactNode[];\n warnings?: React.ReactNode[];\n /** Merged validation status; drives colours, the feedback icon and controls. */\n status?: FormItemStatus;\n hasFeedback?: boolean;\n fieldId?: string;\n /** Same handle as the control's `data-uic-field-id`, for a child that forwards nothing to the DOM. */\n fieldHandle?: string;\n htmlFor?: string;\n className?: string;\n style?: React.CSSProperties;\n hidden?: boolean;\n children?: React.ReactNode;\n}\n\nconst cx = (...names: (string | false | undefined)[]) =>\n names.filter(Boolean).join(\" \");\n\n/**\n * antd grid: `span` / `offset` are 24ths. On the label a span is a fixed\n * basis; on the control it is only a max-width, because antd's horizontal\n * control rule (`flex: 1 1 0`) outranks the column's basis.\n */\nconst GRID_MAX = 24;\nconst colStyle = (\n col: FormItemCol | undefined,\n role: \"label\" | \"control\",\n): React.CSSProperties | undefined => {\n if (!col) return undefined;\n const style: React.CSSProperties = { ...col.style };\n if (col.span !== undefined) {\n const pct = `${(col.span / GRID_MAX) * 100}%`;\n style.maxWidth = pct;\n if (role === \"label\") style.flex = `0 0 ${pct}`;\n }\n if (col.flex !== undefined) {\n style.flex =\n typeof col.flex === \"number\" ? `${col.flex} ${col.flex} auto` : col.flex;\n }\n if (col.offset !== undefined) {\n style.marginInlineStart = `${(col.offset / GRID_MAX) * 100}%`;\n }\n return style;\n};\n\nexport const FormItemVisual: React.FC<FormItemVisualProps> = ({\n label,\n labelTitle,\n tooltip,\n tooltipIcon,\n extra,\n help,\n required,\n requiredMarkType,\n layout = \"vertical\",\n size,\n colon,\n labelAlign,\n labelCol,\n wrapperCol,\n labelWrap,\n errors = [],\n warnings = [],\n status,\n hasFeedback,\n fieldId,\n fieldHandle,\n htmlFor,\n className,\n style,\n hidden,\n children,\n}) => {\n const hasHelp = help !== undefined && help !== null && help !== false;\n // antd's trigger for the reserved-space / margin-offset pair.\n const hasExplain = hasHelp || errors.length > 0 || warnings.length > 0;\n\n /**\n * antd reserves the item's own computed `margin-bottom` inside the\n * additional block while an explain block shows, and cancels it with a\n * negative-margin spacer, so one line of error causes no layout jump. The\n * computed value is read, so a `style.marginBottom` override still nets out.\n */\n const itemRef = React.useRef<HTMLDivElement>(null);\n const extraRef = React.useRef<HTMLDivElement>(null);\n const [marginBottom, setMarginBottom] = React.useState<number | null>(null);\n const [extraHeight, setExtraHeight] = React.useState(0);\n\n React.useLayoutEffect(() => {\n if (hasExplain && itemRef.current) {\n setMarginBottom(\n Number.parseInt(getComputedStyle(itemRef.current).marginBottom, 10) || 0,\n );\n } else {\n setMarginBottom(null);\n }\n }, [hasExplain]);\n\n React.useLayoutEffect(() => {\n setExtraHeight(extra && extraRef.current ? extraRef.current.clientHeight : 0);\n }, [extra]);\n\n const labelNode =\n label === undefined || label === null ? null : (\n <div\n className={cx(\"uic-form-item__label-col\", labelCol?.className)}\n data-align={labelAlign === \"left\" ? \"left\" : undefined}\n data-wrap={labelWrap ? \"\" : undefined}\n style={colStyle(labelCol, \"label\")}\n >\n <label\n htmlFor={htmlFor ?? fieldId}\n className={cx(\n \"uic-form-item__label\",\n required && \"uic-form-item__label--required\",\n )}\n data-required-mark={requiredMarkType}\n // The colon is drawn in every layout and hidden in vertical, as in\n // antd, so a vertical label measures the same.\n data-no-colon={colon === false ? \"\" : undefined}\n title={labelTitle ?? (typeof label === \"string\" ? label : undefined)}\n >\n {label}\n {tooltip ? (\n <Tooltip content={tooltip}>\n <span\n className=\"uic-form-item__tooltip\"\n role=\"button\"\n tabIndex={0}\n aria-label={typeof label === \"string\" ? `${label} — info` : \"info\"}\n // The label is a `<label htmlFor>`: a click inside it would\n // move focus to the control the moment the hint is tapped.\n onClick={(e) => e.preventDefault()}\n >\n {tooltipIcon ?? <CircleHelp size=\"1em\" />}\n </span>\n </Tooltip>\n ) : null}\n </label>\n </div>\n );\n\n // antd's `ErrorList`: `help` replaces the error/warning list and takes the\n // item's status, so `validateStatus=\"error\" help=\"…\"` paints the help red.\n const helpKind =\n status === \"error\" ? \"error\" : status === \"warning\" ? \"warning\" : null;\n const explainRows: Array<{\n key: string;\n node: React.ReactNode;\n kind: \"error\" | \"warning\" | null;\n }> = hasHelp\n ? [{ key: \"help\", node: help, kind: helpKind }]\n : [\n ...errors.map((e, i) => ({ key: `e-${i}`, node: e, kind: \"error\" as const })),\n ...warnings.map((w, i) => ({\n key: `w-${i}`,\n node: w,\n kind: \"warning\" as const,\n })),\n ];\n\n const explainNode = explainRows.length ? (\n <div\n id={fieldId ? `${fieldId}_help` : undefined}\n className=\"uic-form-item__explain\"\n >\n {explainRows.map((row) => (\n <div\n key={row.key}\n className={row.kind ? `uic-form-item__explain-${row.kind}` : undefined}\n >\n {row.node}\n </div>\n ))}\n </div>\n ) : null;\n\n const extraNode = extra ? (\n <div\n ref={extraRef}\n id={fieldId ? `${fieldId}_extra` : undefined}\n className=\"uic-form-item__extra\"\n >\n {extra}\n </div>\n ) : null;\n\n const feedbackNode =\n hasFeedback && status ? (\n <span className=\"uic-form-item__feedback-icon\" data-status={status}>\n {getFeedbackIcon(status)}\n </span>\n ) : null;\n\n const control = (\n <div\n className={cx(\"uic-form-item__control\", wrapperCol?.className)}\n data-span={wrapperCol?.span !== undefined ? \"\" : undefined}\n style={colStyle(wrapperCol, \"control\")}\n >\n <div className=\"uic-form-item__control-input\">\n <div className=\"uic-form-item__control-input-content\">{children}</div>\n {feedbackNode}\n </div>\n {explainNode || extraNode ? (\n <div\n className=\"uic-form-item__additional\"\n style={marginBottom ? { minHeight: marginBottom + extraHeight } : undefined}\n >\n {explainNode}\n {extraNode}\n </div>\n ) : null}\n </div>\n );\n\n return (\n <div\n ref={itemRef}\n className={cx(\"uic-form-item\", className)}\n data-uic-field-item={fieldHandle}\n data-layout={layout}\n data-size={size}\n data-status={status || undefined}\n data-feedback={feedbackNode ? \"\" : undefined}\n data-hidden={hidden ? \"\" : undefined}\n // `display: none` inline too: hiding a field is a correctness property\n // and must hold even without the stylesheet.\n style={hidden ? { display: \"none\", ...style } : style}\n >\n <div className=\"uic-form-item__row\">\n {labelNode}\n {control}\n </div>\n {marginBottom ? (\n <div\n aria-hidden\n className=\"uic-form-item__margin-offset\"\n style={{ marginBottom: -marginBottom }}\n />\n ) : null}\n </div>\n );\n};\n\nexport default FormItemVisual;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormItemVisual2.js","names":[],"sources":["../../../src/components/Form/FormItemVisual.tsx"],"sourcesContent":["/**\n * The presentational shell of a form item: label, control, explain and extra,\n * laid out the way antd lays out `Form.Item` (`layout`, `labelCol` /\n * `wrapperCol` spans, `colon`, `labelAlign`, `labelWrap`, `size`,\n * `hasFeedback`, `validateStatus`), so a form moved onto the engine keeps its\n * geometry. Styles are in `FormItemVisual.css`.\n *\n * .uic-form-item\n * .uic-form-item__row\n * .uic-form-item__label-col\n * label.uic-form-item__label (+ .uic-form-item__tooltip)\n * .uic-form-item__label-extra\n * .uic-form-item__control\n * .uic-form-item__control-input\n * .uic-form-item__control-input-content\n * .uic-form-item__feedback-icon\n * .uic-form-item__additional\n * .uic-form-item__explain (.uic-form-item__explain-error / -warning rows)\n * .uic-form-item__extra\n * .uic-form-item__margin-offset\n */\nimport { CircleHelp } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Tooltip } from \"@astryxdesign/core/Tooltip\";\n\nimport getFeedbackIcon from \"./feedbackIcons\";\nimport \"./FormItemVisual.css\";\n\nexport type FormItemLayout = \"vertical\" | \"horizontal\" | \"inline\";\nexport type FormItemSize = \"small\" | \"middle\" | \"large\";\nexport type FormItemStatus = \"success\" | \"warning\" | \"error\" | \"validating\" | \"\";\n\n/** antd's `Col` props, reduced to `span`, `offset`, `flex`, `className` and `style`. */\nexport interface FormItemCol {\n span?: number;\n offset?: number;\n flex?: string | number;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport interface FormItemVisualProps {\n label?: React.ReactNode;\n /**\n * `<label title>`. Separate from `label` because it is taken from the\n * original prop, before `requiredMark` wraps it: a function mark turns the\n * label into an element and the title would disappear.\n */\n labelTitle?: string;\n /** The tooltip body, rendered behind a help glyph after the label. */\n tooltip?: React.ReactNode;\n /** The tooltip trigger glyph. Defaults to a question mark in a circle. */\n tooltipIcon?: React.ReactNode;\n /**\n * Actions that belong to the label row (e.g. a filter button). Rendered\n * outside the `<label>`, so they stay out of the control's accessible name:\n * at the far end of the row in vertical layout, right after the label\n * otherwise. Needs a `label`.\n */\n labelExtra?: React.ReactNode;\n extra?: React.ReactNode;\n help?: React.ReactNode;\n /** Renders the required marker. Independent of the `required` rule. */\n required?: boolean;\n /**\n * How the marker is drawn: `'optional'` and `'hidden'` hide the asterisk (a\n * function or `'optional'` `requiredMark` puts the hint in the label).\n */\n requiredMarkType?: \"optional\" | \"hidden\";\n layout?: FormItemLayout;\n size?: FormItemSize;\n colon?: boolean;\n labelAlign?: \"left\" | \"right\";\n labelCol?: FormItemCol;\n wrapperCol?: FormItemCol;\n labelWrap?: boolean;\n errors?: React.ReactNode[];\n warnings?: React.ReactNode[];\n /** Merged validation status; drives colours, the feedback icon and controls. */\n status?: FormItemStatus;\n hasFeedback?: boolean;\n fieldId?: string;\n /** Same handle as the control's `data-uic-field-id`, for a child that forwards nothing to the DOM. */\n fieldHandle?: string;\n htmlFor?: string;\n className?: string;\n style?: React.CSSProperties;\n hidden?: boolean;\n children?: React.ReactNode;\n}\n\nconst cx = (...names: (string | false | undefined)[]) =>\n names.filter(Boolean).join(\" \");\n\n/**\n * antd grid: `span` / `offset` are 24ths. On the label a span is a fixed\n * basis; on the control it is only a max-width, because antd's horizontal\n * control rule (`flex: 1 1 0`) outranks the column's basis.\n */\nconst GRID_MAX = 24;\nconst colStyle = (\n col: FormItemCol | undefined,\n role: \"label\" | \"control\",\n): React.CSSProperties | undefined => {\n if (!col) return undefined;\n const style: React.CSSProperties = { ...col.style };\n if (col.span !== undefined) {\n const pct = `${(col.span / GRID_MAX) * 100}%`;\n style.maxWidth = pct;\n if (role === \"label\") style.flex = `0 0 ${pct}`;\n }\n if (col.flex !== undefined) {\n style.flex =\n typeof col.flex === \"number\" ? `${col.flex} ${col.flex} auto` : col.flex;\n }\n if (col.offset !== undefined) {\n style.marginInlineStart = `${(col.offset / GRID_MAX) * 100}%`;\n }\n return style;\n};\n\nexport const FormItemVisual: React.FC<FormItemVisualProps> = ({\n label,\n labelTitle,\n tooltip,\n tooltipIcon,\n labelExtra,\n extra,\n help,\n required,\n requiredMarkType,\n layout = \"vertical\",\n size,\n colon,\n labelAlign,\n labelCol,\n wrapperCol,\n labelWrap,\n errors = [],\n warnings = [],\n status,\n hasFeedback,\n fieldId,\n fieldHandle,\n htmlFor,\n className,\n style,\n hidden,\n children,\n}) => {\n const hasHelp = help !== undefined && help !== null && help !== false;\n // antd's trigger for the reserved-space / margin-offset pair.\n const hasExplain = hasHelp || errors.length > 0 || warnings.length > 0;\n\n /**\n * antd reserves the item's own computed `margin-bottom` inside the\n * additional block while an explain block shows, and cancels it with a\n * negative-margin spacer, so one line of error causes no layout jump. The\n * computed value is read, so a `style.marginBottom` override still nets out.\n */\n const itemRef = React.useRef<HTMLDivElement>(null);\n const extraRef = React.useRef<HTMLDivElement>(null);\n const [marginBottom, setMarginBottom] = React.useState<number | null>(null);\n const [extraHeight, setExtraHeight] = React.useState(0);\n\n React.useLayoutEffect(() => {\n if (hasExplain && itemRef.current) {\n setMarginBottom(\n Number.parseInt(getComputedStyle(itemRef.current).marginBottom, 10) || 0,\n );\n } else {\n setMarginBottom(null);\n }\n }, [hasExplain]);\n\n React.useLayoutEffect(() => {\n setExtraHeight(extra && extraRef.current ? extraRef.current.clientHeight : 0);\n }, [extra]);\n\n const hasLabelExtra =\n labelExtra !== undefined && labelExtra !== null && labelExtra !== false;\n\n const labelNode =\n label === undefined || label === null ? null : (\n <div\n className={cx(\"uic-form-item__label-col\", labelCol?.className)}\n data-align={labelAlign === \"left\" ? \"left\" : undefined}\n data-wrap={labelWrap ? \"\" : undefined}\n data-has-label-extra={hasLabelExtra ? \"\" : undefined}\n style={colStyle(labelCol, \"label\")}\n >\n <label\n htmlFor={htmlFor ?? fieldId}\n className={cx(\n \"uic-form-item__label\",\n required && \"uic-form-item__label--required\",\n )}\n data-required-mark={requiredMarkType}\n // The colon is drawn in every layout and hidden in vertical, as in\n // antd, so a vertical label measures the same.\n data-no-colon={colon === false ? \"\" : undefined}\n title={labelTitle ?? (typeof label === \"string\" ? label : undefined)}\n >\n {label}\n {tooltip ? (\n <Tooltip content={tooltip}>\n <span\n className=\"uic-form-item__tooltip\"\n role=\"button\"\n tabIndex={0}\n aria-label={typeof label === \"string\" ? `${label} — info` : \"info\"}\n // The label is a `<label htmlFor>`: a click inside it would\n // move focus to the control the moment the hint is tapped.\n onClick={(e) => e.preventDefault()}\n >\n {tooltipIcon ?? <CircleHelp size=\"1em\" />}\n </span>\n </Tooltip>\n ) : null}\n </label>\n {hasLabelExtra ? (\n // The colon moves here so it still ends the row in horizontal layout.\n <span\n className=\"uic-form-item__label-extra\"\n data-no-colon={colon === false ? \"\" : undefined}\n >\n {labelExtra}\n </span>\n ) : null}\n </div>\n );\n\n // antd's `ErrorList`: `help` replaces the error/warning list and takes the\n // item's status, so `validateStatus=\"error\" help=\"…\"` paints the help red.\n const helpKind =\n status === \"error\" ? \"error\" : status === \"warning\" ? \"warning\" : null;\n const explainRows: Array<{\n key: string;\n node: React.ReactNode;\n kind: \"error\" | \"warning\" | null;\n }> = hasHelp\n ? [{ key: \"help\", node: help, kind: helpKind }]\n : [\n ...errors.map((e, i) => ({ key: `e-${i}`, node: e, kind: \"error\" as const })),\n ...warnings.map((w, i) => ({\n key: `w-${i}`,\n node: w,\n kind: \"warning\" as const,\n })),\n ];\n\n const explainNode = explainRows.length ? (\n <div\n id={fieldId ? `${fieldId}_help` : undefined}\n className=\"uic-form-item__explain\"\n >\n {explainRows.map((row) => (\n <div\n key={row.key}\n className={row.kind ? `uic-form-item__explain-${row.kind}` : undefined}\n >\n {row.node}\n </div>\n ))}\n </div>\n ) : null;\n\n const extraNode = extra ? (\n <div\n ref={extraRef}\n id={fieldId ? `${fieldId}_extra` : undefined}\n className=\"uic-form-item__extra\"\n >\n {extra}\n </div>\n ) : null;\n\n const feedbackNode =\n hasFeedback && status ? (\n <span className=\"uic-form-item__feedback-icon\" data-status={status}>\n {getFeedbackIcon(status)}\n </span>\n ) : null;\n\n const control = (\n <div\n className={cx(\"uic-form-item__control\", wrapperCol?.className)}\n data-span={wrapperCol?.span !== undefined ? \"\" : undefined}\n style={colStyle(wrapperCol, \"control\")}\n >\n <div className=\"uic-form-item__control-input\">\n <div className=\"uic-form-item__control-input-content\">{children}</div>\n {feedbackNode}\n </div>\n {explainNode || extraNode ? (\n <div\n className=\"uic-form-item__additional\"\n style={marginBottom ? { minHeight: marginBottom + extraHeight } : undefined}\n >\n {explainNode}\n {extraNode}\n </div>\n ) : null}\n </div>\n );\n\n return (\n <div\n ref={itemRef}\n className={cx(\"uic-form-item\", className)}\n data-uic-field-item={fieldHandle}\n data-layout={layout}\n data-size={size}\n data-status={status || undefined}\n data-feedback={feedbackNode ? \"\" : undefined}\n data-hidden={hidden ? \"\" : undefined}\n // `display: none` inline too: hiding a field is a correctness property\n // and must hold even without the stylesheet.\n style={hidden ? { display: \"none\", ...style } : style}\n >\n <div className=\"uic-form-item__row\">\n {labelNode}\n {control}\n </div>\n {marginBottom ? (\n <div\n aria-hidden\n className=\"uic-form-item__margin-offset\"\n style={{ marginBottom: -marginBottom }}\n />\n ) : null}\n </div>\n );\n};\n\nexport default FormItemVisual;\n"],"mappings":"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"}
|
|
@@ -68,6 +68,7 @@ function v({ item: t, isExiting: n, onClose: a }) {
|
|
|
68
68
|
children: t.title
|
|
69
69
|
}),
|
|
70
70
|
icon: t.icon,
|
|
71
|
+
role: t.status === "error" || t.status === "warning" ? "alert" : "status",
|
|
71
72
|
elevation: "high",
|
|
72
73
|
isDismissable: t.isClosable ?? !0,
|
|
73
74
|
onDismiss: () => a?.(_),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NotificationStack2.js","names":[],"sources":["../../../src/components/NotificationStack/NotificationStack.tsx"],"sourcesContent":["/**\n * NotificationStack\n *\n * Floating notices stacked in the bottom-end corner, newest nearest the\n * corner. Each notice is an Astryx `Banner` that can carry what a toast\n * cannot: background-task progress (determinate or not), Cancel / Retry and a\n * navigation action, a collapsible detail, and a body of the caller's own.\n *\n * It is presentational. The caller owns the list: it adds notices, removes\n * them in `onClose`, and updates a notice in place under the same `key`.\n *\n * - `duration` closes a notice after that many seconds; the countdown pauses\n * while the notice is hovered or holds focus. `null` or `0` keeps it open.\n * - An error notice opens its detail (`children`) up front; the reader's own\n * toggle wins after that.\n * - `maxVisible` caps how many render; the newest win, and the rest render as\n * room frees up.\n * - Removed notices slide out before they unmount, unless the reader asked\n * for reduced motion.\n * - A long description scrolls inside its notice, so the dismiss button stays\n * on screen; the stack scrolls once it reaches the top inset.\n *\n * Layout hooks, set on the stack or an ancestor:\n * `--notification-stack-z` (stacking order, default 11000, one above\n * `Modal`'s default band), `--notification-stack-inset-top` (space kept\n * free above the stack, such as an app header, default 0) and\n * `--notification-stack-body-max-height` (one notice's scroll cap, default 30vh).\n *\n * e2e hooks: each notice carries `data-notification-key`, `data-status` and\n * `data-paused`; its title and description carry `data-testid`\n * `notification-title` and `notification-description`.\n *\n * @example\n * <NotificationStack\n * notifications={[{ key: \"upload\", title: \"Uploading\", percent: 40 }]}\n * onClose={(key) => remove(key)}\n * />\n */\nimport {\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type Key,\n type ReactNode,\n} from \"react\";\nimport { Banner, type BannerStatus } from \"@astryxdesign/core/Banner\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { ProgressBar } from \"@astryxdesign/core/ProgressBar\";\nimport { HStack, VStack } from \"@astryxdesign/core/Stack\";\nimport { Text } from \"@astryxdesign/core/Text\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\nimport { MODAL_LIVE_ATTRIBUTE, refreshModalBackground } from \"../Modal/modalStack\";\nimport \"./NotificationStack.css\";\n\n/** Matches the exit animation's budget in NotificationStack.css. */\nconst EXIT_ANIMATION_MS = 200;\n\nexport interface NotificationStackItem {\n key: Key;\n /** Headline. */\n title: ReactNode;\n description?: ReactNode;\n /** Drives the Banner's icon and colour. @default 'info' */\n status?: BannerStatus;\n /** Background-task progress, 0-100. Omit for a notice with no task. */\n percent?: number;\n /** A task that is running with no measurable progress yet. */\n isProgressIndeterminate?: boolean;\n /**\n * Accessible name of the progress bar.\n * @default the title when it is a string, else the catalog's uic.NotificationStack.progress\n */\n progressLabel?: string;\n /** Label of the navigation action (\"View folder\"). Shown with `onAction`. */\n actionText?: string;\n onAction?: () => void;\n /** @default the catalog's uic.common.retry (\"Retry\") */\n retryText?: string;\n onRetry?: () => void;\n /** @default the catalog's uic.common.cancel (\"Cancel\") */\n cancelText?: string;\n onCancel?: () => void;\n /**\n * Seconds until the notice closes itself; `null` or `0` keeps it open until\n * dismissed. The countdown pauses while the notice is hovered or focused.\n */\n duration?: number | null;\n /** @default true */\n isClosable?: boolean;\n /** Overrides the status icon. */\n icon?: ReactNode;\n /** A complete notice body that replaces the title, description and progress. */\n content?: ReactNode;\n /** Collapsible detail below the header. */\n children?: ReactNode;\n}\n\nexport interface NotificationStackProps {\n /** Oldest first; the last entry renders nearest the corner. */\n notifications: Array<NotificationStackItem>;\n /** Fired by the close button and by the auto-close timer. */\n onClose?: (key: Key) => void;\n /** Cap on simultaneously visible notices; the newest win. Unlimited when unset. */\n maxVisible?: number;\n /** Extra class names on the stack. */\n className?: string;\n \"data-testid\"?: string;\n}\n\nfunction NotificationStackItemView({\n item,\n isExiting,\n onClose,\n}: {\n item: NotificationStackItem;\n isExiting: boolean;\n onClose?: (key: Key) => void;\n}) {\n const t = useUicTranslator();\n const { key, duration } = item;\n\n // The timer must not restart when the parent re-creates `onClose`, but\n // firing must still see the latest one.\n const fireClose = useEffectEvent(() => onClose?.(key));\n\n // A notice that closes under the pointer the reader moved there to read it\n // is lost; focus counts too, so keyboard users get the same reprieve.\n // Each holds the countdown on its own: leaving one must not resume it while\n // the other still holds.\n const [isHovered, setIsHovered] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const isPaused = isHovered || isFocused;\n\n // Derived, not a default: a task is updated in place under the same key\n // (pending, then failed), so the item never remounts. `null` means the\n // reader has not touched the disclosure, so it follows the status.\n const isError = (item.status ?? \"info\") === \"error\";\n const [detailOpenByUser, setDetailOpenByUser] = useState<boolean | null>(null);\n const isDetailOpen = detailOpenByUser ?? isError;\n\n // `0` means \"stay open\", not \"close immediately\".\n const autoCloseMs =\n typeof duration === \"number\" && duration > 0 ? duration * 1000 : null;\n // What is left of the countdown, banked by the timer effect's cleanup so a\n // pause and resume continues rather than restarts.\n const remainingMsRef = useRef<number | null>(autoCloseMs);\n\n // A new duration is a new budget. Cleanups run before effects, so this\n // lands after the timer's cleanup banked the old value and before the timer\n // below reads it.\n useEffect(() => {\n remainingMsRef.current = autoCloseMs;\n }, [autoCloseMs]);\n\n useEffect(() => {\n if (autoCloseMs === null || isExiting || isPaused) return;\n const budget = remainingMsRef.current ?? autoCloseMs;\n const startedAt = Date.now();\n const timer = window.setTimeout(() => fireClose(), budget);\n return () => {\n window.clearTimeout(timer);\n remainingMsRef.current = Math.max(0, budget - (Date.now() - startedAt));\n };\n // An effect event is not a dependency; this plugin version predates it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoCloseMs, isExiting, isPaused]);\n\n const hasProgress =\n item.percent !== undefined || item.isProgressIndeterminate === true;\n const hasActions = !!(item.onCancel || item.onRetry || item.onAction);\n const hasOwnContent = item.content != null;\n\n // `wrap` lets the buttons stack inside the end area of a narrow notice\n // instead of pushing the header wider.\n const actions = (\n <HStack gap={2} align=\"center\" justify=\"end\" wrap=\"wrap\">\n {item.onCancel ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.cancelText ?? t(\"uic.common.cancel\")}\n onClick={item.onCancel}\n />\n ) : null}\n {item.onRetry ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n label={item.retryText ?? t(\"uic.common.retry\")}\n onClick={item.onRetry}\n />\n ) : null}\n {item.onAction && item.actionText ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.actionText}\n onClick={item.onAction}\n />\n ) : null}\n </HStack>\n );\n\n return (\n <div\n className=\"uic-notification-stack__item\"\n data-exiting={isExiting ? \"true\" : \"false\"}\n data-notification-key={String(key)}\n data-status={item.status ?? \"info\"}\n data-paused={isPaused ? \"true\" : \"false\"}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n // React's onFocus/onBlur are the delegated focusin/focusout pair, so\n // focus anywhere inside the notice counts.\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n >\n <Banner\n status={item.status ?? \"info\"}\n title={\n hasOwnContent ? (\n item.content\n ) : (\n <span data-testid=\"notification-title\">{item.title}</span>\n )\n }\n icon={item.icon}\n // Banner is normally in flow; a floating notice needs a shadow.\n elevation=\"high\"\n isDismissable={item.isClosable ?? true}\n onDismiss={() => onClose?.(key)}\n endContent={hasActions ? actions : undefined}\n collapsible={{\n isOpen: isDetailOpen,\n onOpenChange: setDetailOpenByUser,\n }}\n description={\n hasOwnContent ? undefined : item.description || hasProgress ? (\n <VStack gap={2} align=\"stretch\">\n {/* The text scrolls; a progress bar outside it stays pinned. */}\n {item.description ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.description === \"string\" ? (\n <Text type=\"supporting\">\n <span data-testid=\"notification-description\">\n {item.description}\n </span>\n </Text>\n ) : (\n item.description\n )}\n </div>\n ) : null}\n {hasProgress ? (\n <ProgressBar\n value={item.percent ?? 0}\n max={100}\n // Hidden: the Banner title already names the task on screen.\n label={\n item.progressLabel ??\n (typeof item.title === \"string\"\n ? item.title\n : t(\"uic.NotificationStack.progress\"))\n }\n isLabelHidden\n hasValueLabel={!item.isProgressIndeterminate}\n isIndeterminate={item.isProgressIndeterminate}\n />\n ) : null}\n </VStack>\n ) : undefined\n }\n >\n {/* A bare string would take Banner's base size and tower over the\n description, so it gets the description's treatment. */}\n {item.children ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.children === \"string\" ? (\n <Text type=\"supporting\">{item.children}</Text>\n ) : (\n item.children\n )}\n </div>\n ) : null}\n </Banner>\n </div>\n );\n}\n\nexport function NotificationStack({\n notifications,\n onClose,\n maxVisible,\n className,\n \"data-testid\": testId,\n}: NotificationStackProps) {\n // Notices that left `notifications` but are still playing their exit.\n const [exiting, setExiting] = useState<Array<NotificationStackItem>>([]);\n const previousVisibleRef = useRef<Array<NotificationStackItem>>([]);\n // One prune timer per exiting key. They outlive the effect that starts\n // them: any re-render (a new array, the exit itself) re-runs that effect,\n // and cancelling there would leave the notice mounted for good.\n const exitTimersRef = useRef(new Map<Key, number>());\n const stackRef = useRef<HTMLDivElement>(null);\n\n const visible = useMemo(\n () => (maxVisible ? notifications.slice(-maxVisible) : notifications),\n [notifications, maxVisible],\n );\n const newestKey = notifications.at(-1)?.key;\n\n // Once the stack is capped it scrolls, and the newest notice is at the\n // scrolled end. Keyed on the newest notice, not the array: a running task\n // rebuilds the array constantly and would yank a reader back down.\n useEffect(() => {\n const el = stackRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [newestKey]);\n\n useEffect(() => {\n const currentKeys = new Set(notifications.map((n) => n.key));\n // Only a notice that was on screen animates out; one closed while hidden\n // behind `maxVisible` would otherwise flash into the corner.\n const removed = previousVisibleRef.current.filter((n) => !currentKeys.has(n.key));\n previousVisibleRef.current = visible;\n if (removed.length === 0) return;\n const removedKeys = new Set(removed.map((n) => n.key));\n setExiting((prev) => [...prev.filter((n) => !removedKeys.has(n.key)), ...removed]);\n const timers = exitTimersRef.current;\n for (const { key } of removed) {\n window.clearTimeout(timers.get(key));\n timers.set(\n key,\n window.setTimeout(() => {\n timers.delete(key);\n setExiting((prev) => prev.filter((n) => n.key !== key));\n }, EXIT_ANIMATION_MS),\n );\n }\n }, [notifications, visible]);\n\n useEffect(() => {\n const timers = exitTimersRef.current;\n return () => {\n for (const timer of timers.values()) window.clearTimeout(timer);\n timers.clear();\n };\n }, []);\n\n const visibleKeys = new Set(visible.map((n) => n.key));\n const stillExiting = exiting.filter((n) => !visibleKeys.has(n.key));\n\n const isRendered = visible.length > 0 || stillExiting.length > 0;\n // The stack is marked to stay reachable over an open Modal; the modal stack\n // re-reads the mark whenever the stack appears or goes.\n useLayoutEffect(() => {\n refreshModalBackground();\n return () => queueMicrotask(refreshModalBackground);\n }, [isRendered]);\n\n if (!isRendered) return null;\n\n return (\n <div\n ref={stackRef}\n className={[\"uic-notification-stack\", className].filter(Boolean).join(\" \")}\n data-testid={testId}\n {...{ [MODAL_LIVE_ATTRIBUTE]: \"\" }}\n // Each Banner announces itself; the container stays out of the tree.\n role=\"presentation\"\n >\n {stillExiting.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting\n onClose={onClose}\n />\n ))}\n {visible.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting={false}\n onClose={onClose}\n />\n ))}\n </div>\n );\n}\n\nNotificationStack.displayName = \"NotificationStack\";\n"],"mappings":";;;;;;;;;;;AA2DA,IAAM,IAAoB;AAsD1B,SAAS,EAA0B,EACjC,SACA,cACA,cAKC;CACD,IAAM,IAAI,EAAiB,GACrB,EAAE,QAAK,gBAAa,GAIpB,IAAY,QAAqB,IAAU,CAAG,CAAC,GAM/C,CAAC,GAAW,KAAgB,EAAS,EAAK,GAC1C,CAAC,GAAW,KAAgB,EAAS,EAAK,GAC1C,IAAW,KAAa,GAKxB,KAAW,EAAK,UAAU,YAAY,SACtC,CAAC,GAAkB,KAAuB,EAAyB,IAAI,GACvE,IAAe,KAAoB,GAGnC,IACJ,OAAO,KAAa,YAAY,IAAW,IAAI,IAAW,MAAO,MAG7D,IAAiB,EAAsB,CAAW;CASxD,AAJA,QAAgB;EACd,EAAe,UAAU;CAC3B,GAAG,CAAC,CAAW,CAAC,GAEhB,QAAgB;EACd,IAAI,MAAgB,QAAQ,KAAa,GAAU;EACnD,IAAM,IAAS,EAAe,WAAW,GACnC,IAAY,KAAK,IAAI,GACrB,IAAQ,OAAO,iBAAiB,EAAU,GAAG,CAAM;EACzD,aAAa;GAEX,AADA,OAAO,aAAa,CAAK,GACzB,EAAe,UAAU,KAAK,IAAI,GAAG,KAAU,KAAK,IAAI,IAAI,EAAU;EACxE;CAGF,GAAG;EAAC;EAAa;EAAW;CAAQ,CAAC;CAErC,IAAM,IACJ,EAAK,YAAY,KAAA,KAAa,EAAK,4BAA4B,IAC3D,IAAa,CAAC,EAAE,EAAK,YAAY,EAAK,WAAW,EAAK,WACtD,IAAgB,EAAK,WAAW,MAIhC,IACJ,kBAAC,GAAD;EAAQ,KAAK;EAAG,OAAM;EAAS,SAAQ;EAAM,MAAK;EAAlD,UAAA;GACG,EAAK,WACJ,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,OAAO,EAAK,cAAc,EAAE,mBAAmB;IAC/C,SAAS,EAAK;GACf,CAAA,IACC;GACH,EAAK,UACJ,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,OAAO,EAAK,aAAa,EAAE,kBAAkB;IAC7C,SAAS,EAAK;GACf,CAAA,IACC;GACH,EAAK,YAAY,EAAK,aACrB,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,OAAO,EAAK;IACZ,SAAS,EAAK;GACf,CAAA,IACC;EACE;;CAGV,OACE,kBAAC,OAAD;EACE,WAAU;EACV,gBAAc,IAAY,SAAS;EACnC,yBAAuB,OAAO,CAAG;EACjC,eAAa,EAAK,UAAU;EAC5B,eAAa,IAAW,SAAS;EACjC,oBAAoB,EAAa,EAAI;EACrC,oBAAoB,EAAa,EAAK;EAGtC,eAAe,EAAa,EAAI;EAChC,cAAc,EAAa,EAAK;EAEhC,UAAA,kBAAC,GAAD;GACE,QAAQ,EAAK,UAAU;GACvB,OACE,IACE,EAAK,UAEL,kBAAC,QAAD;IAAM,eAAY;IAAsB,UAAA,EAAK;GAAY,CAAA;GAG7D,MAAM,EAAK;GAEX,WAAU;GACV,eAAe,EAAK,cAAc;GAClC,iBAAiB,IAAU,CAAG;GAC9B,YAAY,IAAa,IAAU,KAAA;GACnC,aAAa;IACX,QAAQ;IACR,cAAc;GAChB;GACA,aACE,IAAgB,KAAA,IAAY,EAAK,eAAe,IAC9C,kBAAC,GAAD;IAAQ,KAAK;IAAG,OAAM;IAAtB,UAAA,CAEG,EAAK,cACJ,kBAAC,OAAD;KAAK,WAAU;KACZ,UAAA,OAAO,EAAK,eAAgB,WAC3B,kBAAC,GAAD;MAAM,MAAK;MACT,UAAA,kBAAC,QAAD;OAAM,eAAY;OACf,UAAA,EAAK;MACF,CAAA;KACF,CAAA,IAEN,EAAK;IAEJ,CAAA,IACH,MACH,IACC,kBAAC,GAAD;KACE,OAAO,EAAK,WAAW;KACvB,KAAK;KAEL,OACE,EAAK,kBACJ,OAAO,EAAK,SAAU,WACnB,EAAK,QACL,EAAE,gCAAgC;KAExC,eAAA;KACA,eAAe,CAAC,EAAK;KACrB,iBAAiB,EAAK;IACvB,CAAA,IACC,IACE;GACN,CAAA,IAAA,KAAA;GAKL,UAAA,EAAK,WACJ,kBAAC,OAAD;IAAK,WAAU;IACZ,UAAA,OAAO,EAAK,YAAa,WACxB,kBAAC,GAAD;KAAM,MAAK;KAAc,UAAA,EAAK;IAAe,CAAA,IAE7C,EAAK;GAEJ,CAAA,IACH;EACE,CAAA;CACL,CAAA;AAET;AAEA,SAAgB,EAAkB,EAChC,kBACA,YACA,eACA,cACA,eAAe,KACU;CAEzB,IAAM,CAAC,GAAS,KAAc,EAAuC,CAAC,CAAC,GACjE,IAAqB,EAAqC,CAAC,CAAC,GAI5D,IAAgB,kBAAO,IAAI,IAAiB,CAAC,GAC7C,IAAW,EAAuB,IAAI,GAEtC,IAAU,QACP,IAAa,EAAc,MAAM,CAAC,CAAU,IAAI,GACvD,CAAC,GAAe,CAAU,CAC5B,GACM,IAAY,EAAc,GAAG,EAAE,CAAC,EAAE;CAgCxC,AA3BA,QAAgB;EACd,IAAM,IAAK,EAAS;EACpB,AAAI,MAAI,EAAG,YAAY,EAAG;CAC5B,GAAG,CAAC,CAAS,CAAC,GAEd,QAAgB;EACd,IAAM,IAAc,IAAI,IAAI,EAAc,KAAK,MAAM,EAAE,GAAG,CAAC,GAGrD,IAAU,EAAmB,QAAQ,QAAQ,MAAM,CAAC,EAAY,IAAI,EAAE,GAAG,CAAC;EAEhF,IADA,EAAmB,UAAU,GACzB,EAAQ,WAAW,GAAG;EAC1B,IAAM,IAAc,IAAI,IAAI,EAAQ,KAAK,MAAM,EAAE,GAAG,CAAC;EACrD,GAAY,MAAS,CAAC,GAAG,EAAK,QAAQ,MAAM,CAAC,EAAY,IAAI,EAAE,GAAG,CAAC,GAAG,GAAG,CAAO,CAAC;EACjF,IAAM,IAAS,EAAc;EAC7B,KAAK,IAAM,EAAE,YAAS,GAEpB,AADA,OAAO,aAAa,EAAO,IAAI,CAAG,CAAC,GACnC,EAAO,IACL,GACA,OAAO,iBAAiB;GAEtB,AADA,EAAO,OAAO,CAAG,GACjB,GAAY,MAAS,EAAK,QAAQ,MAAM,EAAE,QAAQ,CAAG,CAAC;EACxD,GAAG,CAAiB,CACtB;CAEJ,GAAG,CAAC,GAAe,CAAO,CAAC,GAE3B,QAAgB;EACd,IAAM,IAAS,EAAc;EAC7B,aAAa;GACX,KAAK,IAAM,KAAS,EAAO,OAAO,GAAG,OAAO,aAAa,CAAK;GAC9D,EAAO,MAAM;EACf;CACF,GAAG,CAAC,CAAC;CAEL,IAAM,IAAc,IAAI,IAAI,EAAQ,KAAK,MAAM,EAAE,GAAG,CAAC,GAC/C,IAAe,EAAQ,QAAQ,MAAM,CAAC,EAAY,IAAI,EAAE,GAAG,CAAC,GAE5D,IAAa,EAAQ,SAAS,KAAK,EAAa,SAAS;CAU/D,OAPA,SACE,EAAuB,SACV,eAAe,CAAsB,IACjD,CAAC,CAAU,CAAC,GAEV,IAGH,kBAAC,OAAD;EACE,KAAK;EACL,WAAW,CAAC,0BAA0B,CAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;EACzE,eAAa;GACN,IAAuB;EAE9B,MAAK;EANP,UAAA,CAQG,EAAa,KAAK,MACjB,kBAAC,GAAD;GAEQ;GACN,WAAA;GACS;EACV,GAJM,EAAK,GAIX,CACF,GACA,EAAQ,KAAK,MACZ,kBAAC,GAAD;GAEQ;GACN,WAAW;GACF;EACV,GAJM,EAAK,GAIX,CACF,CACE;MA3BiB;AA6B1B;AAEA,EAAkB,cAAc"}
|
|
1
|
+
{"version":3,"file":"NotificationStack2.js","names":[],"sources":["../../../src/components/NotificationStack/NotificationStack.tsx"],"sourcesContent":["/**\n * NotificationStack\n *\n * Floating notices stacked in the bottom-end corner, newest nearest the\n * corner. Each notice is an Astryx `Banner` that can carry what a toast\n * cannot: background-task progress (determinate or not), Cancel / Retry and a\n * navigation action, a collapsible detail, and a body of the caller's own.\n *\n * It is presentational. The caller owns the list: it adds notices, removes\n * them in `onClose`, and updates a notice in place under the same `key`.\n *\n * - `duration` closes a notice after that many seconds; the countdown pauses\n * while the notice is hovered or holds focus. `null` or `0` keeps it open.\n * - An error notice opens its detail (`children`) up front; the reader's own\n * toggle wins after that.\n * - `maxVisible` caps how many render; the newest win, and the rest render as\n * room frees up.\n * - Removed notices slide out before they unmount, unless the reader asked\n * for reduced motion.\n * - A long description scrolls inside its notice, so the dismiss button stays\n * on screen; the stack scrolls once it reaches the top inset.\n *\n * Layout hooks, set on the stack or an ancestor:\n * `--notification-stack-z` (stacking order, default 11000, one above\n * `Modal`'s default band), `--notification-stack-inset-top` (space kept\n * free above the stack, such as an app header, default 0) and\n * `--notification-stack-body-max-height` (one notice's scroll cap, default 30vh).\n *\n * e2e hooks: each notice carries `data-notification-key`, `data-status` and\n * `data-paused`; its title and description carry `data-testid`\n * `notification-title` and `notification-description`.\n *\n * @example\n * <NotificationStack\n * notifications={[{ key: \"upload\", title: \"Uploading\", percent: 40 }]}\n * onClose={(key) => remove(key)}\n * />\n */\nimport {\n useEffect,\n useEffectEvent,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type Key,\n type ReactNode,\n} from \"react\";\nimport { Banner, type BannerStatus } from \"@astryxdesign/core/Banner\";\nimport { Button } from \"@astryxdesign/core/Button\";\nimport { ProgressBar } from \"@astryxdesign/core/ProgressBar\";\nimport { HStack, VStack } from \"@astryxdesign/core/Stack\";\nimport { Text } from \"@astryxdesign/core/Text\";\n\nimport { useUicTranslator } from \"../../i18n/useUicTranslator\";\nimport { MODAL_LIVE_ATTRIBUTE, refreshModalBackground } from \"../Modal/modalStack\";\nimport \"./NotificationStack.css\";\n\n/** Matches the exit animation's budget in NotificationStack.css. */\nconst EXIT_ANIMATION_MS = 200;\n\nexport interface NotificationStackItem {\n key: Key;\n /** Headline. */\n title: ReactNode;\n description?: ReactNode;\n /** Drives the Banner's icon and colour. @default 'info' */\n status?: BannerStatus;\n /** Background-task progress, 0-100. Omit for a notice with no task. */\n percent?: number;\n /** A task that is running with no measurable progress yet. */\n isProgressIndeterminate?: boolean;\n /**\n * Accessible name of the progress bar.\n * @default the title when it is a string, else the catalog's uic.NotificationStack.progress\n */\n progressLabel?: string;\n /** Label of the navigation action (\"View folder\"). Shown with `onAction`. */\n actionText?: string;\n onAction?: () => void;\n /** @default the catalog's uic.common.retry (\"Retry\") */\n retryText?: string;\n onRetry?: () => void;\n /** @default the catalog's uic.common.cancel (\"Cancel\") */\n cancelText?: string;\n onCancel?: () => void;\n /**\n * Seconds until the notice closes itself; `null` or `0` keeps it open until\n * dismissed. The countdown pauses while the notice is hovered or focused.\n */\n duration?: number | null;\n /** @default true */\n isClosable?: boolean;\n /** Overrides the status icon. */\n icon?: ReactNode;\n /** A complete notice body that replaces the title, description and progress. */\n content?: ReactNode;\n /** Collapsible detail below the header. */\n children?: ReactNode;\n}\n\nexport interface NotificationStackProps {\n /** Oldest first; the last entry renders nearest the corner. */\n notifications: Array<NotificationStackItem>;\n /** Fired by the close button and by the auto-close timer. */\n onClose?: (key: Key) => void;\n /** Cap on simultaneously visible notices; the newest win. Unlimited when unset. */\n maxVisible?: number;\n /** Extra class names on the stack. */\n className?: string;\n \"data-testid\"?: string;\n}\n\nfunction NotificationStackItemView({\n item,\n isExiting,\n onClose,\n}: {\n item: NotificationStackItem;\n isExiting: boolean;\n onClose?: (key: Key) => void;\n}) {\n const t = useUicTranslator();\n const { key, duration } = item;\n\n // The timer must not restart when the parent re-creates `onClose`, but\n // firing must still see the latest one.\n const fireClose = useEffectEvent(() => onClose?.(key));\n\n // A notice that closes under the pointer the reader moved there to read it\n // is lost; focus counts too, so keyboard users get the same reprieve.\n // Each holds the countdown on its own: leaving one must not resume it while\n // the other still holds.\n const [isHovered, setIsHovered] = useState(false);\n const [isFocused, setIsFocused] = useState(false);\n const isPaused = isHovered || isFocused;\n\n // Derived, not a default: a task is updated in place under the same key\n // (pending, then failed), so the item never remounts. `null` means the\n // reader has not touched the disclosure, so it follows the status.\n const isError = (item.status ?? \"info\") === \"error\";\n const [detailOpenByUser, setDetailOpenByUser] = useState<boolean | null>(null);\n const isDetailOpen = detailOpenByUser ?? isError;\n\n // `0` means \"stay open\", not \"close immediately\".\n const autoCloseMs =\n typeof duration === \"number\" && duration > 0 ? duration * 1000 : null;\n // What is left of the countdown, banked by the timer effect's cleanup so a\n // pause and resume continues rather than restarts.\n const remainingMsRef = useRef<number | null>(autoCloseMs);\n\n // A new duration is a new budget. Cleanups run before effects, so this\n // lands after the timer's cleanup banked the old value and before the timer\n // below reads it.\n useEffect(() => {\n remainingMsRef.current = autoCloseMs;\n }, [autoCloseMs]);\n\n useEffect(() => {\n if (autoCloseMs === null || isExiting || isPaused) return;\n const budget = remainingMsRef.current ?? autoCloseMs;\n const startedAt = Date.now();\n const timer = window.setTimeout(() => fireClose(), budget);\n return () => {\n window.clearTimeout(timer);\n remainingMsRef.current = Math.max(0, budget - (Date.now() - startedAt));\n };\n // An effect event is not a dependency; this plugin version predates it.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [autoCloseMs, isExiting, isPaused]);\n\n const hasProgress =\n item.percent !== undefined || item.isProgressIndeterminate === true;\n const hasActions = !!(item.onCancel || item.onRetry || item.onAction);\n const hasOwnContent = item.content != null;\n\n // `wrap` lets the buttons stack inside the end area of a narrow notice\n // instead of pushing the header wider.\n const actions = (\n <HStack gap={2} align=\"center\" justify=\"end\" wrap=\"wrap\">\n {item.onCancel ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.cancelText ?? t(\"uic.common.cancel\")}\n onClick={item.onCancel}\n />\n ) : null}\n {item.onRetry ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n label={item.retryText ?? t(\"uic.common.retry\")}\n onClick={item.onRetry}\n />\n ) : null}\n {item.onAction && item.actionText ? (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n label={item.actionText}\n onClick={item.onAction}\n />\n ) : null}\n </HStack>\n );\n\n return (\n <div\n className=\"uic-notification-stack__item\"\n data-exiting={isExiting ? \"true\" : \"false\"}\n data-notification-key={String(key)}\n data-status={item.status ?? \"info\"}\n data-paused={isPaused ? \"true\" : \"false\"}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n // React's onFocus/onBlur are the delegated focusin/focusout pair, so\n // focus anywhere inside the notice counts.\n onFocus={() => setIsFocused(true)}\n onBlur={() => setIsFocused(false)}\n >\n <Banner\n status={item.status ?? \"info\"}\n title={\n hasOwnContent ? (\n item.content\n ) : (\n <span data-testid=\"notification-title\">{item.title}</span>\n )\n }\n icon={item.icon}\n // Astryx 0.7 no longer derives a live role from `status`; a notice\n // arrives asynchronously, so it keeps the one it had.\n role={item.status === \"error\" || item.status === \"warning\" ? \"alert\" : \"status\"}\n // Banner is normally in flow; a floating notice needs a shadow.\n elevation=\"high\"\n isDismissable={item.isClosable ?? true}\n onDismiss={() => onClose?.(key)}\n endContent={hasActions ? actions : undefined}\n collapsible={{\n isOpen: isDetailOpen,\n onOpenChange: setDetailOpenByUser,\n }}\n description={\n hasOwnContent ? undefined : item.description || hasProgress ? (\n <VStack gap={2} align=\"stretch\">\n {/* The text scrolls; a progress bar outside it stays pinned. */}\n {item.description ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.description === \"string\" ? (\n <Text type=\"supporting\">\n <span data-testid=\"notification-description\">\n {item.description}\n </span>\n </Text>\n ) : (\n item.description\n )}\n </div>\n ) : null}\n {hasProgress ? (\n <ProgressBar\n value={item.percent ?? 0}\n max={100}\n // Hidden: the Banner title already names the task on screen.\n label={\n item.progressLabel ??\n (typeof item.title === \"string\"\n ? item.title\n : t(\"uic.NotificationStack.progress\"))\n }\n isLabelHidden\n hasValueLabel={!item.isProgressIndeterminate}\n isIndeterminate={item.isProgressIndeterminate}\n />\n ) : null}\n </VStack>\n ) : undefined\n }\n >\n {/* A bare string would take Banner's base size and tower over the\n description, so it gets the description's treatment. */}\n {item.children ? (\n <div className=\"uic-notification-stack__body\">\n {typeof item.children === \"string\" ? (\n <Text type=\"supporting\">{item.children}</Text>\n ) : (\n item.children\n )}\n </div>\n ) : null}\n </Banner>\n </div>\n );\n}\n\nexport function NotificationStack({\n notifications,\n onClose,\n maxVisible,\n className,\n \"data-testid\": testId,\n}: NotificationStackProps) {\n // Notices that left `notifications` but are still playing their exit.\n const [exiting, setExiting] = useState<Array<NotificationStackItem>>([]);\n const previousVisibleRef = useRef<Array<NotificationStackItem>>([]);\n // One prune timer per exiting key. They outlive the effect that starts\n // them: any re-render (a new array, the exit itself) re-runs that effect,\n // and cancelling there would leave the notice mounted for good.\n const exitTimersRef = useRef(new Map<Key, number>());\n const stackRef = useRef<HTMLDivElement>(null);\n\n const visible = useMemo(\n () => (maxVisible ? notifications.slice(-maxVisible) : notifications),\n [notifications, maxVisible],\n );\n const newestKey = notifications.at(-1)?.key;\n\n // Once the stack is capped it scrolls, and the newest notice is at the\n // scrolled end. Keyed on the newest notice, not the array: a running task\n // rebuilds the array constantly and would yank a reader back down.\n useEffect(() => {\n const el = stackRef.current;\n if (el) el.scrollTop = el.scrollHeight;\n }, [newestKey]);\n\n useEffect(() => {\n const currentKeys = new Set(notifications.map((n) => n.key));\n // Only a notice that was on screen animates out; one closed while hidden\n // behind `maxVisible` would otherwise flash into the corner.\n const removed = previousVisibleRef.current.filter((n) => !currentKeys.has(n.key));\n previousVisibleRef.current = visible;\n if (removed.length === 0) return;\n const removedKeys = new Set(removed.map((n) => n.key));\n setExiting((prev) => [...prev.filter((n) => !removedKeys.has(n.key)), ...removed]);\n const timers = exitTimersRef.current;\n for (const { key } of removed) {\n window.clearTimeout(timers.get(key));\n timers.set(\n key,\n window.setTimeout(() => {\n timers.delete(key);\n setExiting((prev) => prev.filter((n) => n.key !== key));\n }, EXIT_ANIMATION_MS),\n );\n }\n }, [notifications, visible]);\n\n useEffect(() => {\n const timers = exitTimersRef.current;\n return () => {\n for (const timer of timers.values()) window.clearTimeout(timer);\n timers.clear();\n };\n }, []);\n\n const visibleKeys = new Set(visible.map((n) => n.key));\n const stillExiting = exiting.filter((n) => !visibleKeys.has(n.key));\n\n const isRendered = visible.length > 0 || stillExiting.length > 0;\n // The stack is marked to stay reachable over an open Modal; the modal stack\n // re-reads the mark whenever the stack appears or goes.\n useLayoutEffect(() => {\n refreshModalBackground();\n return () => queueMicrotask(refreshModalBackground);\n }, [isRendered]);\n\n if (!isRendered) return null;\n\n return (\n <div\n ref={stackRef}\n className={[\"uic-notification-stack\", className].filter(Boolean).join(\" \")}\n data-testid={testId}\n {...{ [MODAL_LIVE_ATTRIBUTE]: \"\" }}\n // Each Banner announces itself (its explicit role); the container stays out of the tree.\n role=\"presentation\"\n >\n {stillExiting.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting\n onClose={onClose}\n />\n ))}\n {visible.map((item) => (\n <NotificationStackItemView\n key={item.key}\n item={item}\n isExiting={false}\n onClose={onClose}\n />\n ))}\n </div>\n );\n}\n\nNotificationStack.displayName = \"NotificationStack\";\n"],"mappings":"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"}
|
|
@@ -4,8 +4,8 @@ import { Button as t } from "@astryxdesign/core/Button";
|
|
|
4
4
|
import { Heading as n } from "@astryxdesign/core/Heading";
|
|
5
5
|
import { Text as r } from "@astryxdesign/core/Text";
|
|
6
6
|
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { IconButton as o } from "@astryxdesign/core/IconButton";
|
|
8
|
+
import { Icon as s } from "@astryxdesign/core/Icon";
|
|
9
9
|
//#region src/components/PageHeader/PageHeader.tsx
|
|
10
10
|
function c({ title: c, description: l, actions: u, error: d, onErrorDismiss: f, className: p = "", errorDetail: m, onRetry: h, retryLabel: g, dismissErrorLabel: _ }) {
|
|
11
11
|
let v = e(), y = ["uic-page-header", p].filter(Boolean).join(" ");
|
|
@@ -51,12 +51,12 @@ function c({ title: c, description: l, actions: u, error: d, onErrorDismiss: f,
|
|
|
51
51
|
className: "uic-page-header__error-retry",
|
|
52
52
|
label: g ?? v("uic.common.retry"),
|
|
53
53
|
onClick: h
|
|
54
|
-
}), f && /* @__PURE__ */ i(
|
|
54
|
+
}), f && /* @__PURE__ */ i(o, {
|
|
55
55
|
variant: "ghost",
|
|
56
56
|
size: "sm",
|
|
57
57
|
className: "uic-page-header__error-dismiss",
|
|
58
58
|
label: _ ?? v("uic.PageHeader.dismissError"),
|
|
59
|
-
icon: /* @__PURE__ */ i(
|
|
59
|
+
icon: /* @__PURE__ */ i(s, { icon: "close" }),
|
|
60
60
|
onClick: f
|
|
61
61
|
})]
|
|
62
62
|
})]
|
|
@@ -19,7 +19,7 @@ export interface PagedSelectorSelectedItem {
|
|
|
19
19
|
value: string;
|
|
20
20
|
label: string;
|
|
21
21
|
}
|
|
22
|
-
type Passthrough = Pick<ComplexSelectorProps<string[]>, "label" | "isLabelHidden" | "description" | "isOptional" | "isRequired" | "isDisabled" | "isLoading" | "status" | "statusVariant" | "labelTooltip" | "size" | "variant" | "startIcon" | "width" | "placement" | "alignment" | "onOpenChange" | "
|
|
22
|
+
type Passthrough = Pick<ComplexSelectorProps<string[]>, "label" | "isLabelHidden" | "description" | "isOptional" | "isRequired" | "isDisabled" | "isLoading" | "status" | "statusVariant" | "labelTooltip" | "size" | "variant" | "startIcon" | "width" | "placement" | "alignment" | "onOpenChange" | "containerXstyle" | "containerClassName" | "containerStyle" | "controlXstyle" | "controlClassName" | "controlStyle" | "data-testid">;
|
|
23
23
|
interface PagedSelectorBaseProps extends Passthrough {
|
|
24
24
|
/** The loaded options, every page so far, in order. */
|
|
25
25
|
options: readonly PagedSelectorOption[];
|