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 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Box, Stack, Text } from "@sanity/ui";
import { StringInputProps } from "sanity";
import styles from "./stringInputWithCharacterCount.module.css";

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

const SR_COUNT_HINT_ID = "string-input-with-character-count-input-info";

export const StringInputWithCharacterCount = ({
maxCount,
...defaultProps
}: StringInputWithCharacterCountProps) => {
const characterCount = defaultProps.value?.length ?? 0;
const charactersLeft = maxCount - characterCount;
const isOverLimit = charactersLeft < 0;
const countText = `${Math.abs(charactersLeft)} character${charactersLeft !== 1 ? "s" : ""} ${isOverLimit ? "over limit" : charactersLeft === maxCount ? "allowed" : "left"}`;

return (
<Stack space={3}>
<Box>
{defaultProps.renderDefault({
...defaultProps,
elementProps: {
...defaultProps.elementProps,
"aria-describedby": `${defaultProps.elementProps["aria-describedby"]} ${SR_COUNT_HINT_ID}`,
},
})}
</Box>
<span id={SR_COUNT_HINT_ID} className={styles.srOnly}>
{`You can enter up to ${maxCount} character${charactersLeft !== 1 ? "s" : ""}`}
</span>
<Text
size={1}
muted
className={isOverLimit ? styles.overLimit : ""}
aria-live={"polite"}
>
{countText}
</Text>
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.srOnly {
/* visually hidden input to still receive focus and support screen readers */
/* https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_a_label_element_to_trigger_a_hidden_file_input_element */
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}

.overLimit {
color: rgb(203, 38, 24) !important;
}
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/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/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/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/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/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/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/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
Loading