Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/react/src/components/tags/F0TagRaw/F0TagRaw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { forwardRef } from "react"

import { F0Icon } from "@/components/F0Icon"
import { useTextFormatEnforcer } from "@/lib/text"
import { cn } from "@/lib/utils"

import type { F0TagRawProps } from "./types"

import { BaseTag } from "../internal/BaseTag"

export const F0TagRaw = forwardRef<HTMLDivElement, F0TagRawProps>(
({ text, additionalAccessibleText, icon, onlyIcon, info }, ref) => {
(
{ text, additionalAccessibleText, icon, onlyIcon, info, className },
ref
) => {
useTextFormatEnforcer(
text,
{ disallowEmpty: true },
Expand All @@ -18,7 +22,10 @@ export const F0TagRaw = forwardRef<HTMLDivElement, F0TagRawProps>(
return (
<BaseTag
ref={ref}
className="border-[1px] border-solid border-f1-border-secondary"
className={cn(
"border-[1px] border-solid border-f1-border-secondary",
className
)}
left={
icon ? (
<F0Icon
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/components/tags/F0TagRaw/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export type F0TagRawProps = {
* Info text to display an i icon and a tooltip next to the tag
*/
info?: string
/**
* Extra classes merged onto the tag (e.g. to give it a background).
*/
className?: string
} & (
| {
icon: IconType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,11 @@ export const defaultTranslations = {
questionType: "Question type",
questionOptions: "Question options",
actions: "Actions",
locked: "Locked",
lockedSectionNotice:
"These questions are predefined and can't be edited, moved, or removed.",
lockedQuestionNotice:
"This question is predefined and can't be edited or removed.",
sectionTitlePlaceholder: "Section title",
lastQuestionDialogTitle: "Remove last question from section",
lastQuestionDialogDescription:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ function SurveyAnsweringFormInline({
labels,
useUpload,
datasets,
hideResourceHeader = false,
}: SurveyAnsweringFormInlineReadonlyProps) {
const { t } = useI18n()

Expand Down Expand Up @@ -439,13 +440,15 @@ function SurveyAnsweringFormInline({
datasets={datasets}
>
<div className="mx-auto flex w-full max-w-3xl flex-col">
<div className="mb-6">
<ResourceHeader
title={title}
description={description}
{...resourceHeader}
/>
</div>
{!hideResourceHeader && (
<div className="mb-6">
<ResourceHeader
title={title}
description={description}
{...resourceHeader}
/>
</div>
)}
{loading ? (
<SurveyAllQuestionsLoadingSkeleton />
) : !hasQuestions ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,31 @@ describe("SurveyAnsweringForm", () => {
})
})

describe("question notice", () => {
it("does not render an authoring notice in the answering form", () => {
render(
<SurveyAnsweringForm
{...defaultProps}
elements={[
{
type: "question",
question: {
id: "q1",
title: "Name",
type: "text" as const,
lockedNote: { description: "Author-only notice" },
},
},
]}
onSubmit={vi.fn()}
/>
)

expect(screen.getByText("Name")).toBeInTheDocument()
expect(screen.queryByText("Author-only notice")).not.toBeInTheDocument()
})
})

describe("validation on submit", () => {
it("does not call onSubmit when required fields are empty", async () => {
const onSubmit = vi.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ function buildFieldForQuestion(
description: q.description,
type: q.type,
required: q.required,
locked: q.locked,
}

switch (q.type) {
Expand Down Expand Up @@ -679,7 +678,7 @@ export function useSurveyFormSchema(

if (mode === "all-questions") {
sections[sectionId] = {
title: section.title,
title: section.title ?? "",
description: section.description,
withInset: true,
}
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/sds/surveys/SurveyAnsweringForm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ interface SurveyAnsweringFormDialogProps extends SurveyAnsweringFormSharedProps
/** Inline mode: read-only rendering embedded in the page, no dialog */
interface SurveyAnsweringFormInlineProps extends SurveyAnsweringFormSharedProps {
inline: true
/**
* Hide the built-in ResourceHeader (title + description). Useful when the
* embedding page already renders its own resource header above the form.
*/
hideResourceHeader?: boolean
mode?: never
module?: never
position?: never
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const QuestionItem = ({
setDraggedItemId(null)
}

const questionLocked = item.question.locked || containingSection?.locked
const questionLocked = containingSection?.locked
const dragEnabled = !disabled && !answering && !questionLocked

return (
Expand All @@ -66,25 +66,34 @@ export const QuestionItem = ({
<div
className={cn(
"group/element flex flex-row items-start gap-1",
// Mirror the drag-and-drop gutter (handle w-6 + gap-1 ≈ 28px) on
// the right so every card keeps the same width.
!disabled && !answering && "pr-7",
isDragging && "cursor-grabbing"
)}
>
{!disabled && !answering && (
<div
className={cn(
"mt-2 flex aspect-square w-6 scale-75 items-center opacity-0 hover:opacity-40 group-hover/element:opacity-40",
!isDragging && "cursor-grab",
!dragEnabled && "cursor-not-allowed"
)}
onPointerDown={(e) => {
if (dragEnabled) {
dragControls.start(e)
}
}}
>
<F0Icon icon={Handle} size="sm" />
</div>
)}
{!disabled &&
!answering &&
(questionLocked ? (
// Blocked question: drop the drag affordance but keep the handle's
// gutter so the card stays the same width and alignment as the
// editable questions around it.
<div className="mt-2 aspect-square w-6 scale-75" aria-hidden />
) : (
<div
className={cn(
"mt-2 flex aspect-square w-6 scale-75 items-center opacity-0 hover:opacity-40 group-hover/element:opacity-40",
!isDragging && "cursor-grab"
)}
onPointerDown={(e) => {
if (dragEnabled) {
dragControls.start(e)
}
}}
>
<F0Icon icon={Handle} size="sm" />
</div>
))}
<QuestionComponent
{...({
...item.question,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ export const SectionHeaderItem = ({
setDraggedItemId(null)
}

const dragEnabled = !disabled && !answering && !item.section.locked

return (
<Reorder.Item
value={item}
Expand All @@ -56,25 +54,32 @@ export const SectionHeaderItem = ({
<div
className={cn(
"flex flex-row items-start gap-1 w-full",
// Mirror the drag-and-drop gutter (handle w-6 + gap-1 ≈ 28px) on
// the right so the header aligns with the cards' width.
!disabled && !answering && "pr-7",
isDragging && "cursor-grabbing"
)}
>
{!disabled && !answering && (
<div
className={cn(
"mt-2 flex aspect-square w-6 scale-75 items-center opacity-0 hover:opacity-40 group-hover/element:opacity-40",
!isDragging && "cursor-grab",
!dragEnabled && "cursor-not-allowed"
)}
onPointerDown={(e) => {
if (dragEnabled) {
{!disabled &&
!answering &&
(item.section.locked ? (
// Blocked section: drop the drag affordance but keep the
// handle's gutter so the header stays aligned with the
// editable rows around it.
<div className="mt-2 aspect-square w-6 scale-75" aria-hidden />
) : (
<div
className={cn(
"mt-2 flex aspect-square w-6 scale-75 items-center opacity-0 hover:opacity-40 group-hover/element:opacity-40",
!isDragging && "cursor-grab"
)}
onPointerDown={(e) => {
dragControls.start(e)
}
}}
>
<F0Icon icon={Handle} size="sm" />
</div>
)}
}}
>
<F0Icon icon={Handle} size="sm" />
</div>
))}
<SectionComponent {...item.section} hideQuestions />
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,7 @@ export const useTableOfContentItems = (
icon: getQuestionIcon(question.type as QuestionType),
onClick: handleItemClick,
...(!disabled &&
!answering &&
!question.locked && {
!answering && {
otherActions: buildQuestionActions(
question.id,
question.type as QuestionType,
Expand Down
Loading
Loading