From 91952b76ff5813571a252dc99944912caf7b05da Mon Sep 17 00:00:00 2001 From: LuLaValva Date: Tue, 8 Oct 2024 12:39:40 -0700 Subject: [PATCH 1/2] feat(file-preview-cards): add preview card/cards --- .../examples/with-mock-uploads.marko | 52 ++++ .../examples/with-preview-cards.marko | 35 +++ .../ebay-file-input/file-input.stories.ts | 29 +++ .../ebay-file-preview-card-group/README.md | 16 ++ .../ebay-file-preview-card-group/component.ts | 36 +++ .../examples/default.marko | 21 ++ .../file-preview-card-group.stories.ts | 117 +++++++++ .../ebay-file-preview-card-group/index.marko | 33 +++ .../ebay-file-preview-card-group/style.ts | 1 + .../ebay-file-preview-card/README.md | 16 ++ .../ebay-file-preview-card/component.ts | 56 ++++ .../examples/default.marko | 6 + .../file-preview-card.stories.ts | 246 ++++++++++++++++++ .../ebay-file-preview-card/index.marko | 107 ++++++++ .../ebay-file-preview-card/style.ts | 1 + 15 files changed, 772 insertions(+) create mode 100644 src/components/ebay-file-input/examples/with-mock-uploads.marko create mode 100644 src/components/ebay-file-input/examples/with-preview-cards.marko create mode 100644 src/components/ebay-file-preview-card-group/README.md create mode 100644 src/components/ebay-file-preview-card-group/component.ts create mode 100644 src/components/ebay-file-preview-card-group/examples/default.marko create mode 100644 src/components/ebay-file-preview-card-group/file-preview-card-group.stories.ts create mode 100644 src/components/ebay-file-preview-card-group/index.marko create mode 100644 src/components/ebay-file-preview-card-group/style.ts create mode 100644 src/components/ebay-file-preview-card/README.md create mode 100644 src/components/ebay-file-preview-card/component.ts create mode 100644 src/components/ebay-file-preview-card/examples/default.marko create mode 100644 src/components/ebay-file-preview-card/file-preview-card.stories.ts create mode 100644 src/components/ebay-file-preview-card/index.marko create mode 100644 src/components/ebay-file-preview-card/style.ts diff --git a/src/components/ebay-file-input/examples/with-mock-uploads.marko b/src/components/ebay-file-input/examples/with-mock-uploads.marko new file mode 100644 index 000000000..f4dbf5f32 --- /dev/null +++ b/src/components/ebay-file-input/examples/with-mock-uploads.marko @@ -0,0 +1,52 @@ +import type { FileInputEvent } from "../component-browser"; + +static async function mockFetch() { + await new Promise((resolve) => setTimeout(resolve, Math.random() * 5000)); + return `https://fakeurl.com/${Math.random().toString(36).substring(7)}`; +} + +class { + declare state: { + files: [File, string | undefined][]; + }; + + onCreate() { + this.state = { + files: [], + }; + } + + handleInput({ files }: FileInputEvent) { + const fileList = Array.from(files); + this.state.files = this.state.files.concat(fileList.map((file) => [file, undefined])); + for (const file of fileList) { + mockFetch().then((url) => { + const index = this.state.files.findIndex(([f]) => f === file); + this.state.files = [ + ...this.state.files.slice(0, index), + [file, url], + ...this.state.files.slice(index + 1) + ]; + }) + } + } + + handleDelete(index: number) { + this.state.files = [ + ...this.state.files.slice(0, index), + ...this.state.files.slice(index + 1) + ]; + } +} + + <@header class="subtitleClass" id="subtitleId"> +

Multiple files

+ + Browse files +
+ + + + <@card file=file status=url ? undefined : "uploading"/> + + diff --git a/src/components/ebay-file-input/examples/with-preview-cards.marko b/src/components/ebay-file-input/examples/with-preview-cards.marko new file mode 100644 index 000000000..40d08e41c --- /dev/null +++ b/src/components/ebay-file-input/examples/with-preview-cards.marko @@ -0,0 +1,35 @@ +import type { FileInputEvent } from "../component-browser"; +class { + declare state: { + files: File[]; + }; + + onCreate() { + this.state = { + files: [], + }; + } + + handleInput({ files }: FileInputEvent) { + this.state.files = this.state.files.concat(Array.from(files)); + } + + handleDelete(index: number) { + this.state.files = [ + ...this.state.files.slice(0, index), + ...this.state.files.slice(index + 1) + ]; + } +} + + <@header class="subtitleClass" id="subtitleId"> +

Multiple files

+ + Browse files +
+ + + + <@card file=file/> + + diff --git a/src/components/ebay-file-input/file-input.stories.ts b/src/components/ebay-file-input/file-input.stories.ts index b67181697..7ecf2da9b 100644 --- a/src/components/ebay-file-input/file-input.stories.ts +++ b/src/components/ebay-file-input/file-input.stories.ts @@ -4,6 +4,10 @@ import Readme from "./README.md"; import component from "./index.marko"; import DefaultTemplate from "./examples/default.marko"; import DefaultCode from "./examples/default.marko?raw"; +import WithPreviewCardsTemplate from "./examples/with-preview-cards.marko"; +import WithPreviewCardsCode from "./examples/with-preview-cards.marko?raw"; +import WithMockUploadsTemplate from "./examples/with-mock-uploads.marko"; +import WithMockUploadsCode from "./examples/with-mock-uploads.marko?raw"; const Template = (args) => ({ input: addRenderBodies(args), @@ -67,3 +71,28 @@ Default.parameters = { }, }; +export const WithPreviewCards = (args) => ({ + input: args, + component: WithPreviewCardsTemplate, +}); +WithPreviewCards.args = {}; +WithPreviewCards.parameters = { + docs: { + source: { + code: WithPreviewCardsCode, + }, + }, +}; + +export const WithMockUploads = (args) => ({ + input: args, + component: WithMockUploadsTemplate, +}); +WithMockUploads.args = {}; +WithMockUploads.parameters = { + docs: { + source: { + code: WithMockUploadsCode, + }, + }, +}; diff --git a/src/components/ebay-file-preview-card-group/README.md b/src/components/ebay-file-preview-card-group/README.md new file mode 100644 index 000000000..3b4ea1ce5 --- /dev/null +++ b/src/components/ebay-file-preview-card-group/README.md @@ -0,0 +1,16 @@ +

+ + ebay-file-preview-card + + + DS v2.1.0 + +

+ +Group of file preview cards, primarily used alongside `ebay-file-input`. + +## Examples and Documentation + +- [Storybook](https://ebay.github.io/ebayui-core/?path=/story/media-ebay-file-preview-card-group) +- [Storybook Docs](https://ebay.github.io/ebayui-core/?path=/docs/media-ebay-file-preview-card-group) +- [Code Examples](https://github.com/eBay/ebayui-core/tree/master/src/components/ebay-file-preview-card-group/examples) diff --git a/src/components/ebay-file-preview-card-group/component.ts b/src/components/ebay-file-preview-card-group/component.ts new file mode 100644 index 000000000..e8cf1fb31 --- /dev/null +++ b/src/components/ebay-file-preview-card-group/component.ts @@ -0,0 +1,36 @@ +import { WithNormalizedProps } from "../../global"; +import type { + FilePreviewCardEvent, + Input as FilePreviewCardInput, +} from "../ebay-file-preview-card/component"; + +interface FilePreviewCardGroupInput { + card?: Marko.RepeatableAttrTag; + "a11y-cancel-upload-text"?: FilePreviewCardInput["a11y-cancel-upload-text"]; + "delete-text"?: FilePreviewCardInput["delete-text"]; + "menu-actions"?: FilePreviewCardInput["menu-actions"]; + "a11y-show-more-text"?: FilePreviewCardInput["a11y-show-more-text"]; + "on-menu-action"?: (index: number, event: FilePreviewCardEvent) => void; + "on-delete"?: (index: number) => void; + "on-cancel"?: (index: number) => void; +} + +export type Input = WithNormalizedProps; + +export interface State { + showing: number; +} + +const SHOW_AMOUNT = 15; + +class FilePreviewCardGroup extends Marko.Component { + onCreate() { + this.state = { showing: SHOW_AMOUNT }; + } + + showMore() { + this.state.showing += SHOW_AMOUNT; + } +} + +export default FilePreviewCardGroup; diff --git a/src/components/ebay-file-preview-card-group/examples/default.marko b/src/components/ebay-file-preview-card-group/examples/default.marko new file mode 100644 index 000000000..e42250535 --- /dev/null +++ b/src/components/ebay-file-preview-card-group/examples/default.marko @@ -0,0 +1,21 @@ +$ const files = [ + { + name: "file-name.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", + }, + { + name: "file-name.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", + }, +]; + + + <@card file=file/> + + diff --git a/src/components/ebay-file-preview-card-group/file-preview-card-group.stories.ts b/src/components/ebay-file-preview-card-group/file-preview-card-group.stories.ts new file mode 100644 index 000000000..523c6d8e0 --- /dev/null +++ b/src/components/ebay-file-preview-card-group/file-preview-card-group.stories.ts @@ -0,0 +1,117 @@ +import { tagToString } from "../../../.storybook/storybook-code-source"; +import { addRenderBodies } from "../../../.storybook/utils"; +import Readme from "./README.md"; +import component from "./index.marko"; + +const Template = (args) => ({ + input: addRenderBodies(args), +}); + +export default { + title: "media/ebay-file-preview-card-group", + component, + parameters: { + docs: { + description: { + component: Readme, + }, + }, + }, + argTypes: { + card: { + name: "@card", + table: { + category: "@attribute tags", + }, + description: + "A repeatable attribute tag for each file preview card", + }, + a11yCancelUploadText: { + type: "string", + control: { type: "text" }, + description: + "a11y text for cancel upload button, applied to all cards", + }, + deleteText: { + type: "string", + control: { type: "text" }, + description: "Text for delete button, applied to all cards", + }, + menuActions: { + type: "array", + description: "List of menu actions, applied to all cards", + control: { type: "object" }, + }, + a11yShowMoreText: { + type: "string", + control: { type: "text" }, + description: "a11y text for show more button, applied to all cards", + }, + "onMenu-action": { + action: "on-menu-action", + description: + "Triggered when an action is selected from the menu on a card", + table: { + category: "Events", + defaultValue: { + summary: + "index, eventName, event /* from ebay-menu-button */", + }, + }, + }, + onDelete: { + action: "on-delete", + description: + "Triggered when the delete button is clicked on a card", + table: { + category: "Events", + defaultValue: { + summary: "index", + }, + }, + }, + onCancel: { + action: "on-cancel", + description: + "Triggered when the cancel button is clicked on a card", + table: { + category: "Events", + defaultValue: { + summary: "index", + }, + }, + }, + }, +}; + +const frogImage = { + name: "frog.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", +}; + +export const Default = Template.bind({}); +Default.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + card: [ + { + file: { ...frogImage }, + }, + { + file: { ...frogImage }, + }, + { + file: { ...frogImage }, + }, + ], +}; + +export const ManyCards = Template.bind({}); +ManyCards.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + card: [...Array(35)].map(() => ({ + file: { ...frogImage }, + })), +}; diff --git a/src/components/ebay-file-preview-card-group/index.marko b/src/components/ebay-file-preview-card-group/index.marko new file mode 100644 index 000000000..78245b7ef --- /dev/null +++ b/src/components/ebay-file-preview-card-group/index.marko @@ -0,0 +1,33 @@ +$ const { card = [], a11yCancelUploadText, deleteText, menuActions, a11yShowMoreText } = input; +$ const cards = [...card]; +$ const notShowing = cards.length - state.showing; + +
+
    + 0 ? state.showing : cards.length) - 1> + $ const cardInput = cards[i]; +
  • + +
  • + + 0)> +
  • + +
  • + +
+
diff --git a/src/components/ebay-file-preview-card-group/style.ts b/src/components/ebay-file-preview-card-group/style.ts new file mode 100644 index 000000000..d13ccffd1 --- /dev/null +++ b/src/components/ebay-file-preview-card-group/style.ts @@ -0,0 +1 @@ +import "@ebay/skin/file-preview-card-group"; diff --git a/src/components/ebay-file-preview-card/README.md b/src/components/ebay-file-preview-card/README.md new file mode 100644 index 000000000..c917b30a2 --- /dev/null +++ b/src/components/ebay-file-preview-card/README.md @@ -0,0 +1,16 @@ +

+ + ebay-file-preview-card + + + DS v2.1.0 + +

+ +Preview card for files, primarily used alongside `ebay-file-preview-card-group` and `ebay-file-input`. + +## Examples and Documentation + +- [Storybook](https://ebay.github.io/ebayui-core/?path=/story/media-ebay-file-preview-card) +- [Storybook Docs](https://ebay.github.io/ebayui-core/?path=/docs/media-ebay-file-preview-card) +- [Code Examples](https://github.com/eBay/ebayui-core/tree/master/src/components/ebay-file-preview-card/examples) diff --git a/src/components/ebay-file-preview-card/component.ts b/src/components/ebay-file-preview-card/component.ts new file mode 100644 index 000000000..8f22d569f --- /dev/null +++ b/src/components/ebay-file-preview-card/component.ts @@ -0,0 +1,56 @@ +import type { AttrString } from "marko/tags-html"; +import { WithNormalizedProps } from "../../global"; +import { MenuButtonEvent } from "../ebay-menu-button/component"; + +export interface FilePreviewCardEvent { + file: File; + menuAction: FilePreviewCardMenuAction; + originalEvent: Event; +} + +export type FilePreviewCardMenuAction = { + event: string; + label: string; +}; + +interface FilePreviewCardInput { + "a11y-cancel-upload-text"?: AttrString; + "delete-text"?: AttrString; + file: + | File + | { + name: string; + type?: File["type"]; + src?: string; + }; + status?: "uploading"; + "label-text"?: AttrString; + "menu-actions"?: FilePreviewCardMenuAction[]; + "show-more"?: number; + "a11y-show-more-text"?: AttrString; + "on-menu-action"?: (name: string, event: MenuButtonEvent) => void; + "on-show-more"?: () => void; + "on-delete"?: () => void; + "on-cancel"?: () => void; +} + +export type Input = WithNormalizedProps; + +class FilePreviewCard extends Marko.Component { + handleMenuSelect(event: MenuButtonEvent) { + const index = event.checked?.[0]; + const eventName = + this.input.menuActions && + index !== undefined && + index in this.input.menuActions + ? this.input.menuActions[index].event + : null; + if (eventName) { + this.emit("menu-action", eventName, event); + } else { + this.emit("delete"); + } + } +} + +export default FilePreviewCard; diff --git a/src/components/ebay-file-preview-card/examples/default.marko b/src/components/ebay-file-preview-card/examples/default.marko new file mode 100644 index 000000000..c3e96a065 --- /dev/null +++ b/src/components/ebay-file-preview-card/examples/default.marko @@ -0,0 +1,6 @@ +$ const file = { + name: "file-name.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", +}; + diff --git a/src/components/ebay-file-preview-card/file-preview-card.stories.ts b/src/components/ebay-file-preview-card/file-preview-card.stories.ts new file mode 100644 index 000000000..b8b5a8cb9 --- /dev/null +++ b/src/components/ebay-file-preview-card/file-preview-card.stories.ts @@ -0,0 +1,246 @@ +import { tagToString } from "../../../.storybook/storybook-code-source"; +import { addRenderBodies } from "../../../.storybook/utils"; +import Readme from "./README.md"; +import component from "./index.marko"; + +const Template = (args) => ({ + input: addRenderBodies(args), +}); + +export default { + title: "media/ebay-file-preview-card", + component, + parameters: { + docs: { + description: { + component: Readme, + }, + }, + }, + argTypes: { + a11yCancelUploadText: { + type: "string", + control: { type: "text" }, + description: "a11y text for cancel upload button", + }, + deleteText: { + type: "string", + control: { type: "text" }, + description: "Text for delete button", + }, + file: { + type: "object", + description: + "File object, can be raw platform `File` or an object containing `name`, `type`, and a `src` for the preview", + table: { + category: "File", + }, + }, + status: { + type: "string", + control: { + type: "select", + options: ["uploading"], + }, + description: + 'Status of the file, can be `"uploading"` or `undefined`', + }, + labelText: { + type: "string", + control: { type: "text" }, + description: "Text to display in the label", + }, + menuActions: { + type: "array", + description: + "Array of menu actions, containing `event` and `label`", + table: { + category: "Menu Actions", + }, + }, + showMore: { + type: "number", + control: { type: "number" }, + description: + 'Passing a number here will convert the card to a "show more" card', + }, + "onMenu-action": { + action: "on-menu-action", + description: "Triggered when an action is selected from the menu. ", + table: { + category: "Events", + defaultValue: { + summary: "name, event /* from ebay-menu-button */", + }, + }, + }, + "onShow-more": { + action: "on-show-more", + description: "Triggered when the show more button is clicked", + table: { + category: "Events", + defaultValue: { + summary: "", + }, + }, + }, + onDelete: { + action: "on-delete", + description: "Triggered when the delete button is clicked", + table: { + category: "Events", + defaultValue: { + summary: "", + }, + }, + }, + onCancel: { + action: "on-cancel", + description: "Triggered when the cancel button is clicked", + table: { + category: "Events", + defaultValue: { + summary: "", + }, + }, + }, + }, +}; + +export const Uploading = Template.bind({}); +Uploading.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + file: { + name: "file-name.jpg", + type: "image/jpeg", + }, + status: "uploading", +}; + +Uploading.parameters = { + docs: { + source: { + code: tagToString("ebay-file-preview-card", Uploading.args), + }, + }, +}; + +export const Image = Template.bind({}); +Image.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + file: { + name: "file-name.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", + }, +}; + +Image.parameters = { + docs: { + source: { + code: tagToString("ebay-file-preview-card", Image.args), + }, + }, +}; + +export const Video = Template.bind({}); +Video.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + file: { + name: "file-name.mov", + type: "video/quicktime", + src: "https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4", + }, + labelText: "10:30:21", +}; + +Video.parameters = { + docs: { + source: { + code: tagToString("ebay-file-preview-card", Video.args), + }, + }, +}; + +export const MultipleMenuActions = Template.bind({}); +MultipleMenuActions.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + file: { + name: "file-name.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", + }, + menuActions: [ + { + event: "edit", + label: "Edit", + }, + { + event: "download", + label: "Download", + }, + ], +}; + +MultipleMenuActions.parameters = { + docs: { + source: { + code: tagToString( + "ebay-file-preview-card", + MultipleMenuActions.args, + ), + }, + }, +}; + +export const Document = Template.bind({}); +Document.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + file: { + name: "file-name.csv", + type: "text/csv", + }, + menuActions: [ + { + event: "edit", + label: "Edit", + }, + ], +}; + +Document.parameters = { + docs: { + source: { + code: tagToString( + "ebay-file-preview-card", + MultipleMenuActions.args, + ), + }, + }, +}; + +export const ShowMore = Template.bind({}); +ShowMore.args = { + a11yCancelUploadText: "Cancel upload", + deleteText: "Delete", + a11yShowMoreText: "Show more", + showMore: 15, + file: { + name: "file-name.jpg", + type: "image/jpeg", + src: "https://ir.ebaystatic.com/cr/v/c01/skin/docs/tb-real-square-pic.jpg", + }, +}; + +ShowMore.parameters = { + docs: { + source: { + code: tagToString("ebay-file-preview-card", ShowMore.args), + }, + }, +}; diff --git a/src/components/ebay-file-preview-card/index.marko b/src/components/ebay-file-preview-card/index.marko new file mode 100644 index 000000000..3ba01f182 --- /dev/null +++ b/src/components/ebay-file-preview-card/index.marko @@ -0,0 +1,107 @@ +$ const { + a11yCancelUploadText, + deleteText, + file: rawFile, + showMore, + status, + labelText, + menuActions, + a11yShowMoreText, + ...htmlInput +} = input; +$ let file = rawFile as Exclude; +$ const type = ( + rawFile.type?.startsWith("image") + ? "image" + : rawFile.type?.startsWith("video") + ? "video" + : undefined +); +$ if (rawFile instanceof File) { + file = { + name: rawFile.name, + type, + src: type ? URL.createObjectURL(rawFile) : undefined, + }; +} +$ file.type = type; + +
+
+ + + + + file.name + + +
+ +
+ + +${showMore} + +
+ + +
+ + + + + + + + + + <@item value=action.event> + ${action.label} + + + + + + + + + + +
+ +
+ + + + + ${labelText} + + + + ${file.name + .substring(file.name.lastIndexOf(".") + 1) + .toUpperCase()} + + +
+ +
+
diff --git a/src/components/ebay-file-preview-card/style.ts b/src/components/ebay-file-preview-card/style.ts new file mode 100644 index 000000000..8abe7df7f --- /dev/null +++ b/src/components/ebay-file-preview-card/style.ts @@ -0,0 +1 @@ +import "@ebay/skin/file-preview-card"; From 01cbee4770baa82118d39dfd2db8539095480977 Mon Sep 17 00:00:00 2001 From: LuLaValva Date: Tue, 8 Oct 2024 12:49:23 -0700 Subject: [PATCH 2/2] chore: add changeset --- .changeset/cool-zebras-think.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cool-zebras-think.md diff --git a/.changeset/cool-zebras-think.md b/.changeset/cool-zebras-think.md new file mode 100644 index 000000000..1b60361a2 --- /dev/null +++ b/.changeset/cool-zebras-think.md @@ -0,0 +1,5 @@ +--- +"@ebay/ebayui-core": minor +--- + +Add ebay-file-preview-card and ebay-file-preview-card-group