Skip to content

feat(corner-onament): add new component for corner oranment #4008

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

Closed
wants to merge 11 commits into from
Closed
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
6 changes: 6 additions & 0 deletions .changeset/warm-snakes-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/corner-ornament": major
"@twilio-paste/core": minor
---

[Corner Ornament] Release a new component that controls the posiitoning of another elements in relation to a parent component to be displayed as a corner ornament
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"/packages/paste-color-contrast-utils",
"/packages/paste-core/components/combobox",
"/packages/paste-core/primitives/combobox",
"/packages/paste-core/components/corner-ornament",
"/packages/paste-customization",
"/packages/paste-core/components/data-grid",
"/packages/paste-libraries/data-visualization",
Expand Down
5 changes: 5 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ module.exports = {
"error_counter",
"neutral_counter",
"notification_counter",
// these are position names we use as keys in style objects
"top_end",
"top_start",
"bottom_end",
"bottom_start",
// unstable props are allowed
"^unstable_",
// this is a temporary prop, if the console patch is removed from components this can be removed too
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"build": "yarn prebuild && yarn nx run-many --target=build --exclude @twilio-paste/website @twilio-paste/theme-designer @twilio-paste/nextjs-template @twilio-paste/token-contrast-checker",
"build:js": "yarn prebuild && yarn nx run-many --target=build:js --exclude @twilio-paste/website @twilio-paste/theme-designer",
"build:typedocs": "yarn prebuild && yarn nx run-many --target=build:typedocs",
"build:typedocs:clean":"rm -rf .nx/cache && yarn build && yarn build:typedocs",
"build:typedocs:clean": "rm -rf .nx/cache && yarn build && yarn build:typedocs",
"build:core": "yarn nx run @twilio-paste/core:build",
"build:codemods": "yarn nx run @twilio-paste/codemods:build",
"build:tokens": "yarn nx run @twilio-paste/design-tokens:tokens",
Expand Down
3 changes: 3 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@
"MultiselectCombobox": "@twilio-paste/core/combobox",
"useCombobox": "@twilio-paste/core/combobox",
"useMultiselectCombobox": "@twilio-paste/core/combobox",
"CornerOrnament": "@twilio-paste/core/corner-ornament",
"CornerOrnamentContainer": "@twilio-paste/core/corner-ornament",
"CornerOrnamentElement": "@twilio-paste/core/corner-ornament",
"DataGrid": "@twilio-paste/core/data-grid",
"DataGridBody": "@twilio-paste/core/data-grid",
"DataGridCell": "@twilio-paste/core/data-grid",
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { render } from "@testing-library/react";
import { Avatar } from "@twilio-paste/avatar";
import { Box } from "@twilio-paste/box";
import { LogoTwilioIcon } from "@twilio-paste/icons/esm/LogoTwilioIcon";
import { IconSizeOptions } from "@twilio-paste/style-props";
import * as React from "react";

import {
CornerOrnament,
CornerOrnamentContainer,
CornerOrnamentElement,
CornerOrnamentPosition,
CornerOrnamentType,
} from "../src";

const ExampleCornerOrnament: React.FC<{
size?: IconSizeOptions;
position?: CornerOrnamentPosition;
type?: CornerOrnamentType;
}> = ({ size, type }) => (
<CornerOrnamentContainer
data-testid="cornerOrnamentContainer"
cornerOrnamentType={type || "dot"}
size={size || "sizeIcon50"}
>
<CornerOrnamentElement data-testid="cornerOrnamentElement">
<Avatar data-testid="ornament-element" size="sizeIcon50" src="./avatars/avatar8.png" name="GitHub avatar" />
</CornerOrnamentElement>
<CornerOrnament data-testid="cornerOrnament">
<Box data-testid="ornament">
<LogoTwilioIcon decorative size="sizeIcon40" />
</Box>
</CornerOrnament>
</CornerOrnamentContainer>
);

const CustomizedCornerOrnament: React.FC<{
size?: IconSizeOptions;
position?: CornerOrnamentPosition;
type?: CornerOrnamentType;
}> = ({ size, type }) => (
<CornerOrnamentContainer
data-testid="cornerOrnamentContainer"
cornerOrnamentType={type || "dot"}
size={size || "sizeIcon50"}
element="CUSTOMIZED_CORNER_ORNAMENT_CONTAINER"
>
<CornerOrnamentElement data-testid="cornerOrnamentElement" element="CUSTOMIZED_CORNER_ORNAMENT_ELEMENT">
<Avatar data-testid="ornament-element" size="sizeIcon50" src="./avatars/avatar8.png" name="GitHub avatar" />
</CornerOrnamentElement>
<CornerOrnament data-testid="cornerOrnament" element="CUSTOMIZED_CORNER_ORNAMENT">
<Box data-testid="ornament">
<LogoTwilioIcon decorative size="sizeIcon40" />
</Box>
</CornerOrnament>
</CornerOrnamentContainer>
);

describe("CornerOrnament", () => {
it("should render", () => {
const { getByTestId } = render(<ExampleCornerOrnament />);
expect(getByTestId("ornament-element")).toBeDefined();
expect(getByTestId("ornament")).toBeDefined();
});

it("should throw errors for unsupported size and type combinations", () => {
expect(() => {
render(<ExampleCornerOrnament size="sizeIcon30" type="avatar" />);
}).toThrow();
});

describe("Customization", () => {
it("should set element data attribute", () => {
const { getByTestId } = render(<ExampleCornerOrnament />);
expect(getByTestId("cornerOrnamentContainer").getAttribute("data-paste-element")).toEqual(
"CORNER_ORNAMENT_CONTAINER",
);
expect(getByTestId("cornerOrnamentElement").getAttribute("data-paste-element")).toEqual(
"CORNER_ORNAMENT_ELEMENT",
);
expect(getByTestId("cornerOrnament").getAttribute("data-paste-element")).toEqual("CORNER_ORNAMENT");
});

it("should set custom element data attribute", () => {
const { getByTestId } = render(<CustomizedCornerOrnament />);
expect(getByTestId("cornerOrnamentContainer").getAttribute("data-paste-element")).toEqual(
"CUSTOMIZED_CORNER_ORNAMENT_CONTAINER",
);
expect(getByTestId("cornerOrnamentElement").getAttribute("data-paste-element")).toEqual(
"CUSTOMIZED_CORNER_ORNAMENT_ELEMENT",
);
expect(getByTestId("cornerOrnament").getAttribute("data-paste-element")).toEqual("CUSTOMIZED_CORNER_ORNAMENT");
});
});
});
3 changes: 3 additions & 0 deletions packages/paste-core/components/corner-ornament/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const {build} = require('../../../../tools/build/esbuild');

build(require('./package.json'));
61 changes: 61 additions & 0 deletions packages/paste-core/components/corner-ornament/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@twilio-paste/corner-ornament",
"version": "0.0.0",
"category": "data display",
"status": "production",
"description": "A component used to apply masking to an element and position another element as an ornament.",
"author": "Twilio Inc.",
"license": "MIT",
"main:dev": "src/index.tsx",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "yarn clean && NODE_ENV=production node build.js && tsc",
"build:js": "NODE_ENV=development node build.js",
"build:typedocs": "tsx ../../../../tools/build/generate-type-docs",
"clean": "rm -rf ./dist",
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/animation-library": "^2.0.0",
"@twilio-paste/box": "^10.2.0",
"@twilio-paste/color-contrast-utils": "^5.0.0",
"@twilio-paste/customization": "^8.1.1",
"@twilio-paste/design-tokens": "^10.3.0",
"@twilio-paste/style-props": "^9.1.1",
"@twilio-paste/styling-library": "^3.0.0",
"@twilio-paste/theme": "^11.0.1",
"@twilio-paste/types": "^6.0.0",
"@twilio-paste/uid-library": "^2.0.0",
"@types/react": "^16.8.6 || ^17.0.2 || ^18.0.27",
"@types/react-dom": "^16.8.6 || ^17.0.2 || ^18.0.10",
"react": "^16.8.6 || ^17.0.2 || ^18.0.0",
"react-dom": "^16.8.6 || ^17.0.2 || ^18.0.0"
},
"devDependencies": {
"@twilio-paste/animation-library": "^2.0.0",
"@twilio-paste/box": "^10.2.0",
"@twilio-paste/color-contrast-utils": "^5.0.0",
"@twilio-paste/customization": "^8.1.1",
"@twilio-paste/design-tokens": "^10.3.0",
"@twilio-paste/style-props": "^9.1.1",
"@twilio-paste/styling-library": "^3.0.0",
"@twilio-paste/theme": "^11.0.1",
"@twilio-paste/types": "^6.0.0",
"@twilio-paste/uid-library": "^2.0.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"tsx": "^4.0.0",
"typescript": "^4.9.4"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Box, BoxStyleProps, safelySpreadBoxProps } from "@twilio-paste/box";
import { IconSizeOptions } from "@twilio-paste/style-props";
import * as React from "react";

import { useCornerOrnamentContext } from "./CornerOrnamentContext";
import { CornerOrnamentPosition, CornerOrnamentProps, CornerOrnamentType } from "./types";

type OrnamentSpacingMapping = Record<
CornerOrnamentType,
Record<CornerOrnamentPosition, Partial<Record<IconSizeOptions, BoxStyleProps>>>
>;

export const CornerOrnament = React.forwardRef<HTMLDivElement, CornerOrnamentProps>(
({ element = "CORNER_ORNAMENT", ...props }, ref) => {
const { cornerOrnamentType, size, position } = useCornerOrnamentContext();

const Positions: OrnamentSpacingMapping = {
badge: {
bottom_end: {
sizeIcon80: { top: "space70", left: "space70" },
},
top_end: {
sizeIcon80: { left: "space70", top: "space0" },
},
},
avatar: {
bottom_end: {
sizeIcon70: { top: "space60", left: "space60" },
sizeIcon80: { top: "space70", left: "space70" },
},
top_end: {
sizeIcon70: { left: "space50", top: "space0" },
sizeIcon80: { left: "space70", top: "space0" },
},
},
icon: {
bottom_end: {
sizeIcon30: { top: "space40", left: "space40" },
sizeIcon40: { top: "0.91rem", left: "0.91rem" },
sizeIcon50: { top: "1.05rem", left: "1.05rem" },
sizeIcon70: { top: "space60", left: "space60" },
sizeIcon80: { top: "space70", left: "space70" },
},
top_end: {
sizeIcon30: { left: "space40", top: "space0" },
sizeIcon40: { left: "space50", top: "space0" },
sizeIcon50: { left: "space60", top: "space0" },
sizeIcon70: { left: "space60", top: "space0" },
sizeIcon80: { left: "space70", top: "space0" },
},
},
dot: {
bottom_end: {
sizeIcon30: { top: "space40", left: "space40" },
sizeIcon40: { top: "space50", left: "space50" },
sizeIcon50: { top: "space60", left: "space60" },
},
top_end: {
sizeIcon30: { left: "space40", top: "space0" },
sizeIcon40: { left: "space50", top: "space0" },
sizeIcon50: { left: "space60", top: "space0" },
},
},
};

if (!Positions[cornerOrnamentType][position][size]) {
throw new Error(
"[Paste: CornerOrnament] the size/position/type combination you have chosen is not currently supported. Please refer to our guildinges in our docs or raise a new disucssion to get this supported at https://github.com/twilio-labs/paste/discussions.",
);
}

return (
<Box
{...safelySpreadBoxProps(props)}
position="absolute"
element={element}
ref={ref}
{...Positions[cornerOrnamentType][position][size]}
>
{props.children}
</Box>
);
},
);

CornerOrnament.displayName = "CornerOrnament";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Box, safelySpreadBoxProps } from "@twilio-paste/box";
import * as React from "react";

import { CornerOrnamentContext } from "./CornerOrnamentContext";
import { CornerOrnamentContainerProps } from "./types";

export const CornerOrnamentContainer = React.forwardRef<HTMLDivElement, CornerOrnamentContainerProps>(
({ size, cornerOrnamentType, position = "bottom_end", element = "CORNER_ORNAMENT_CONTAINER", ...props }, ref) => {
return (
<CornerOrnamentContext.Provider value={{ size, cornerOrnamentType, position }}>
<Box {...safelySpreadBoxProps(props)} position="relative" element={element} ref={ref}>
{props.children}
</Box>
</CornerOrnamentContext.Provider>
);
},
);

CornerOrnamentContainer.displayName = "CornerOrnamentContainer";
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

import { CornerOrnamentContextInterface } from "./types";

export const CornerOrnamentContext = React.createContext<CornerOrnamentContextInterface | null>(null);

export const useCornerOrnamentContext = (): CornerOrnamentContextInterface => {
const context = React.useContext(CornerOrnamentContext);
if (!context) {
throw new Error("useCornerOrnamentContext must be used with CornerOrnamentContextProvider");
}
return context;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Box, safelySpreadBoxProps } from "@twilio-paste/box";
import { useUID } from "@twilio-paste/uid-library";
import * as React from "react";

import { useCornerOrnamentContext } from "./CornerOrnamentContext";
import { BadgeBottomEndPath, DotBottomEndPath } from "./Masks";
import { CornerOrnamentElementProps, CornerOrnamentPosition, CornerOrnamentType } from "./types";

export const CornerOrnamentElement = React.forwardRef<HTMLDivElement, CornerOrnamentElementProps>(
({ padding, element = "CORNER_ORNAMENT_ELEMENT", ...props }, ref) => {
const id = useUID();
const { cornerOrnamentType, position, size } = useCornerOrnamentContext();

const ClipPathMapping: Record<CornerOrnamentType, Record<CornerOrnamentPosition, string>> = {
badge: {
bottom_end: BadgeBottomEndPath,
top_end: BadgeBottomEndPath,
},
dot: {
bottom_end: DotBottomEndPath,
top_end: DotBottomEndPath,
},
icon: {
bottom_end: BadgeBottomEndPath,
top_end: BadgeBottomEndPath,
},
avatar: {
bottom_end: BadgeBottomEndPath,
top_end: BadgeBottomEndPath,
},
};

return (
<Box
{...safelySpreadBoxProps(props)}
style={{
clipPath: `url("#${id}")`,
}}
element={element}
ref={ref}
size={size}
>
<Box padding={padding || "space0"}>{props.children}</Box>
<Box as="svg" height={0} width={0} position="absolute" top={0} left={0}>
<defs>
<clipPath id={id} clipPathUnits="objectBoundingBox">
{<path d={ClipPathMapping[cornerOrnamentType][position]} />}
</clipPath>
</defs>
</Box>
</Box>
);
},
);

CornerOrnamentElement.displayName = "CornerOrnamentElement";
4 changes: 4 additions & 0 deletions packages/paste-core/components/corner-ornament/src/Masks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const BadgeBottomEndPath =
"M 1.023 0.499 C 1.032 0.147 0.844 -0.031 0.497 -0.029 S -0.02 0.185 -0.021 0.497 s 0.179 0.525 0.504 0.527 c 0.0121 0 0.024 -0.0004 0.045 0 A 0.3436 0.3436 0 0 1 0.5 0.8452 C 0.5 0.6546 0.6546 0.5 0.8452 0.5 H 1.024 Z";
export const DotBottomEndPath =
"M 1.023 0.499 C 1.027 0.204 0.796 -0.029 0.497 -0.029 S -0.02 0.185 -0.021 0.497 s 0.2239 0.5 0.504 0.527 c 0.0121 0 0.024 -0.0004 0.045 0 A 0.3436 0.3436 0 0 1 0.5 0.8452 C 0.5 0.6546 0.6546 0.5 0.8452 0.5 H 1.024 Z";
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading