Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3 - custom string input with character count #529

Merged
merged 6 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
26 changes: 26 additions & 0 deletions studio/components/StringInputWithCharacterCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Box, Stack, Text } from "@sanity/ui";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before we merge can you make sure it passes wcag 2.1? (plenty of studio is just bleh, but we can give it a try since it's a custom component
https://designsystem.digital.gov/components/character-count/accessibility-tests/

Copy link
Contributor Author

@mathiazom mathiazom Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated now to show the remaining characters, with a red text when over limit. Also added aria-describedby to announce allowed number of characters. aria-live has been set on the counter, but I have struggled to get the screen reader to announce it reliably. Seems to be intercepted by text field description, which might be Sanity's fault. Default text input is slightly more reliable based on quick testing.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I believe this is on Sanity. Great job though!

import { StringInputProps } from "sanity";

type StringInputWithCharacterCountProps = StringInputProps & {
maxCount?: number;
};

export const StringInputWithCharacterCount = ({
maxCount,
...defaultProps
}: StringInputWithCharacterCountProps) => {
const characterCount = defaultProps.value?.length ?? 0;
const isPlural =
(maxCount !== undefined && maxCount !== 1) ||
(maxCount === undefined && characterCount !== 1);

return (
<Stack space={3}>
<Box>{defaultProps.renderDefault(defaultProps)}</Box>
<Text size={1} muted>
{characterCount}
{maxCount && `/${maxCount}`} character{isPlural ? "s" : ""}
</Text>
</Stack>
);
};
7 changes: 6 additions & 1 deletion studio/schemas/builders/pageBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import testimonals from "../objects/sections/testimonials";
import imageSection from "../objects/sections/image";
import grid from "../objects/sections/grid";
import contactForm from "../objects/sections/form";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

export const pageBuilderID = "pageBuilder";

Expand All @@ -25,7 +26,11 @@ const pageBuilder = defineType({
description:
"Enter a distinctive name for the dynamic page to help content editors easily identify and manage it. This name is used internally and is not visible on your website.",
type: "string",
validation: (Rule) => Rule.required().max(30),
validation: (rule) => rule.required().max(30),
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: 30 }),
},
}),
pageSlug,
seo,
Expand Down
19 changes: 16 additions & 3 deletions studio/schemas/documents/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineField, defineType } from "sanity";
import seo from "../objects/seo";
import { pageSlug } from "../schemaTypes/slug";
import { title } from "../fields/text";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

export const blogId = "blog";

Expand All @@ -16,7 +17,11 @@ const blog = defineType({
description:
"Enter a distinctive name for the page to help content editors easily identify and manage it. This name is used internally and is not visible on your website.",
type: "string",
validation: (Rule) => Rule.required().max(30),
validation: (rule) => rule.required().max(30),
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: 30 }),
},
}),
pageSlug,
seo,
Expand All @@ -28,7 +33,11 @@ const blog = defineType({
"Enter the label used to refer to all posts regardless of their category. This label will be displayed in the filter section on the main blog page. Examples include 'news', 'stories', or 'posts'.",
type: "string",
initialValue: "All posts",
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required().max(20),
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: 20 }),
},
}),
defineField({
name: "categories",
Expand All @@ -48,7 +57,11 @@ const blog = defineType({
description:
"The name of the category. This will be displayed on the website and used for organizing blog posts.",
type: "string",
validation: (Rule) => Rule.required().min(1).max(100),
validation: (rule) => rule.required().min(1).max(100),
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: 100 }),
},
}),
],
}),
Expand Down
6 changes: 6 additions & 0 deletions studio/schemas/documents/companyInfo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineType, defineField } from "sanity";
import seo from "../objects/seo";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

export const companyInfoID = "companyInfo";

Expand All @@ -24,6 +25,11 @@ const companyInfo = defineType({
type: "string",
title: "Site Name",
description: "The name of your website.",
validation: (rule) => rule.max(60),
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: 60 }),
},
}),
defineField({
name: "defaultLanguage",
Expand Down
6 changes: 6 additions & 0 deletions studio/schemas/documents/companyLocation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineField, defineType } from "sanity";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

export const companyLocationID = "companyLocation";
export const companyLocationNameID = "companyLocationName";
Expand All @@ -13,6 +14,11 @@ const companyLocation = defineType({
name: companyLocationNameID,
type: "string",
title: "Location",
validation: (rule) => rule.max(50),
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: 50 }),
},
}),
],
});
Expand Down
4 changes: 2 additions & 2 deletions studio/schemas/documents/navigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const navigationManager = defineType({
"Add links to the main menu. These links will appear at the top of your website and help visitors navigate to important sections. The first Call to Action (CTA) will be styled as a primary link button. Note: The order in which you add the links here is how they will be displayed on the website.",
type: "array",
of: [{ type: linkID }, { type: callToActionFieldID }],
validation: (Rule) =>
Rule.custom((links) => {
validation: (rule) =>
rule.custom((links) => {
if (!Array.isArray(links)) return true;
const ctaCount = links.filter(
(link) => link._type === callToActionFieldID,
Expand Down
12 changes: 6 additions & 6 deletions studio/schemas/documents/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const posts = defineType({
description: "Select the date and time when this post will be published.",
type: "datetime",
initialValue: () => new Date().toISOString(),
validation: (Rule) =>
Rule.required().custom((date, context) => {
validation: (rule) =>
rule.required().custom((date, context) => {
// Ensure date is not undefined or null
if (!date) return "The publish date is required.";

Expand Down Expand Up @@ -57,7 +57,7 @@ const posts = defineType({
components: {
input: CategorySelector,
},
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
}),
defineField({
name: "lead",
Expand All @@ -69,15 +69,15 @@ const posts = defineType({
defineField({
...richText,
description: "Enter the introductory text for the post.",
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
}),
defineField({
...image,
description: "Upload a featured image for the post.",
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
}),
],
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
}),
defineField({
...richText,
Expand Down
9 changes: 7 additions & 2 deletions studio/schemas/fields/categories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineField } from "sanity";
import { defineField, StringInputProps } from "sanity";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

export const categoriesId = "categories";

Expand All @@ -11,7 +12,11 @@ const categories = defineField({
name: "category",
type: "string",
title: "Category",
validation: (Rule) => Rule.required().min(1).max(100),
validation: (rule) => rule.required().min(1).max(100),
components: {
input: (props: StringInputProps) =>
StringInputWithCharacterCount({ ...props, maxCount: 100 }),
},
},
],
});
Expand Down
34 changes: 17 additions & 17 deletions studio/schemas/fields/media.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineField } from "sanity";
import { defineField, StringInputProps } from "sanity";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

export enum ImageAlignment {
Left = "left",
Expand All @@ -15,20 +16,25 @@ const alignmentOptions = [
{ title: "Right", value: ImageAlignment.Right },
];

const imageAltField = defineField({
name: "alt",
type: "string",
title: "Alternative Text",
description:
"Provide a description of the image for accessibility. Leave empty if the image is purely decorative.",
validation: (rule) => rule.max(100),
components: {
input: (props: StringInputProps) =>
StringInputWithCharacterCount({ ...props, maxCount: 100 }),
},
});

const image = defineField({
name: "image",
title: "Image",
type: "image",
options: { hotspot: true },
fields: [
{
name: "alt",
type: "string",
title: "Alternative Text",
description:
"Provide a description of the image for accessibility. Leave empty if the image is purely decorative.",
},
],
fields: [imageAltField],
});

export const imageExtended = defineField({
Expand All @@ -37,13 +43,7 @@ export const imageExtended = defineField({
type: "image",
options: { hotspot: true },
fields: [
{
name: "alt",
type: "string",
title: "Alternative Text",
description:
"Provide a description of the image for accessibility. Leave empty if the image is purely decorative.",
},
imageAltField,
{
name: "imageAlignment",
title: "Image Alignment",
Expand Down
14 changes: 11 additions & 3 deletions studio/schemas/fields/text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StringRule, defineField } from "sanity";
import { StringInputWithCharacterCount } from "../../components/StringInputWithCharacterCount";

enum titleID {
basic = "basicTitle",
Expand All @@ -22,8 +23,8 @@ const createField = ({
isRequired = false,
maxLength = 60,
}: CreateFieldProps) => {
const validationRules = (Rule: StringRule) => {
let rules = Rule.max(maxLength);
const validationRules = (rule: StringRule) => {
let rules = rule.max(maxLength);
if (isRequired) {
rules = rules.required();
}
Expand All @@ -35,6 +36,10 @@ const createField = ({
title,
type: "string",
validation: validationRules,
components: {
input: (props) =>
StringInputWithCharacterCount({ ...props, maxCount: maxLength }),
},
});
};

Expand All @@ -53,7 +58,10 @@ export const optionalSubtitle = defineField({
name: subtitleID.optional,
title: "Subtitle",
type: "string",
validation: (Rule) => Rule.max(60),
validation: (rule) => rule.max(60),
components: {
input: (props) => StringInputWithCharacterCount({ ...props, maxCount: 60 }),
},
});

export const richTextID = "richText";
mathiazom marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
8 changes: 4 additions & 4 deletions studio/schemas/objects/compensations/benefitsByLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const benefitType = defineField({
layout: BENEFIT_TYPES.length > 5 ? "dropdown" : "radio",
},
initialValue: BENEFIT_TYPE_BASIC_VALUE,
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
});

export const benefitsByLocation = defineField({
Expand All @@ -45,7 +45,7 @@ export const benefitsByLocation = defineField({
...location,
description:
"Select the office location for which you are entering benefits information. Each location must be unique.",
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
},
defineField({
name: "benefits",
Expand Down Expand Up @@ -93,8 +93,8 @@ export const benefitsByLocation = defineField({
},
},
],
validation: (Rule) =>
Rule.custom((benefitsByLocation) => {
validation: (rule) =>
rule.custom((benefitsByLocation) => {
const isNotDuplicate: boolean = checkForDuplicateLocations(
benefitsByLocation as DocumentWithLocation[] | undefined,
);
Expand Down
13 changes: 7 additions & 6 deletions studio/schemas/objects/compensations/bonusesByLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const bonusesByLocation = defineField({
...location,
description:
"Select the company location for which you are entering the yearly bonus data. Each location must be unique.",
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
},
defineField({
name: "yearlyBonuses",
Expand All @@ -43,16 +43,17 @@ export const bonusesByLocation = defineField({
description:
"The calendar year for which this bonus was given",
type: "number",
validation: (Rule) => Rule.required(),
validation: (rule) => rule.required(),
}),
defineField({
name: "bonus",
title: "Bonus",
description:
"Enter the bonus amount for this year. Ensure the amount is positive and reflective of the compensation package for this location.",
type: "number",
validation: (Rule) =>
Rule.required()
validation: (rule) =>
rule
.required()
.min(0)
.error("Please enter a positive bonus amount."),
}),
Expand Down Expand Up @@ -95,8 +96,8 @@ export const bonusesByLocation = defineField({
},
}),
],
validation: (Rule) =>
Rule.custom((bonusesByLocation) => {
validation: (rule) =>
rule.custom((bonusesByLocation) => {
const isNotDuplicate: boolean = checkForDuplicateLocations(
bonusesByLocation as DocumentWithLocation[] | undefined,
);
Expand Down
7 changes: 4 additions & 3 deletions studio/schemas/objects/compensations/pension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ export const pension = defineField({
type: "number",
initialValue: 7,
description: `Specify the percentage of the pension provided by Variant for employees. The value should be a positive number and will be used to calculate the pension amount.`,
validation: (Rule) => [
Rule.min(0)
validation: (rule) => [
rule
.min(0)
.max(100)
.error("The pension percentage must be a number between 0 and 100."),
Rule.custom((value, context) => {
rule.custom((value, context) => {
if (
context.document?.showSalaryCalculator &&
(value === undefined || value === null)
Expand Down
Loading