From 2360d220f1a0f5177177d7255f0f7f52ea48cfc1 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Tue, 18 Jul 2023 09:45:32 -0400 Subject: [PATCH 01/26] feat: add inline-edit package --- apps/remix/app/data.server.ts | 1 + .../app/routes/components.inline-edit.tsx | 5 ++ package.json | 1 + packages/core/package.json | 1 + packages/core/src/index.ts | 2 + packages/inline-edit/README.md | 14 ++++++ .../inline-edit/__tests__/InlineEdit.cy.tsx | 13 +++++ .../inline-edit/__tests__/InlineEdit.spec.tsx | 11 +++++ packages/inline-edit/package.json | 47 +++++++++++++++++++ packages/inline-edit/src/InlineEdit.tsx | 38 +++++++++++++++ packages/inline-edit/src/index.ts | 2 + .../inline-edit/src/styles/InlineEdit.css.ts | 33 +++++++++++++ .../stories/InlineEdit.stories.tsx | 20 ++++++++ packages/inline-edit/tsconfig.build.json | 7 +++ pnpm-lock.yaml | 33 +++++++++++++ tsconfig.json | 1 + 16 files changed, 229 insertions(+) create mode 100644 apps/remix/app/routes/components.inline-edit.tsx create mode 100644 packages/inline-edit/README.md create mode 100644 packages/inline-edit/__tests__/InlineEdit.cy.tsx create mode 100644 packages/inline-edit/__tests__/InlineEdit.spec.tsx create mode 100644 packages/inline-edit/package.json create mode 100644 packages/inline-edit/src/InlineEdit.tsx create mode 100644 packages/inline-edit/src/index.ts create mode 100644 packages/inline-edit/src/styles/InlineEdit.css.ts create mode 100644 packages/inline-edit/stories/InlineEdit.stories.tsx create mode 100644 packages/inline-edit/tsconfig.build.json diff --git a/apps/remix/app/data.server.ts b/apps/remix/app/data.server.ts index cd60095c7..6362b176d 100644 --- a/apps/remix/app/data.server.ts +++ b/apps/remix/app/data.server.ts @@ -18,6 +18,7 @@ export async function getComponents() { { to: 'components/icon', name: 'Icon' }, { to: 'components/icon-button', name: 'IconButton' }, { to: 'components/inline', name: 'Inline' }, + { to: 'components/inline-edit', name: 'InlineEdit' }, { to: 'components/markdown', name: 'Markdown' }, { to: 'components/menu', name: 'Menu' }, { to: 'components/modal', name: 'Modal' }, diff --git a/apps/remix/app/routes/components.inline-edit.tsx b/apps/remix/app/routes/components.inline-edit.tsx new file mode 100644 index 000000000..14b6fca01 --- /dev/null +++ b/apps/remix/app/routes/components.inline-edit.tsx @@ -0,0 +1,5 @@ +import { InlineEdit } from '@launchpad-ui/core'; + +export default function Index() { + return A lovely InlineEdit component.; +} diff --git a/package.json b/package.json index 1652e1eba..ebcbca352 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "@vanilla-extract/css": "^1.12.0", + "@vanilla-extract/recipes": "^0.4.0", "@vanilla-extract/vite-plugin": "^3.8.2", "@vitejs/plugin-react-swc": "^3.3.1", "@vitest/coverage-v8": "^0.33.0", diff --git a/packages/core/package.json b/packages/core/package.json index 69b867f45..7285862bb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,6 +51,7 @@ "@launchpad-ui/focus-trap": "workspace:~", "@launchpad-ui/form": "workspace:~", "@launchpad-ui/inline": "workspace:~", + "@launchpad-ui/inline-edit": "workspace:~", "@launchpad-ui/markdown": "workspace:~", "@launchpad-ui/menu": "workspace:~", "@launchpad-ui/modal": "workspace:~", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c25c725c5..a4db57f2d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -105,6 +105,7 @@ export type { StackProps } from '@launchpad-ui/stack'; export type { Space } from '@launchpad-ui/types'; export type { InlineProps } from '@launchpad-ui/inline'; export type { ColumnProps, ColumnsProps } from '@launchpad-ui/columns'; +export type { InlineEditProps } from '@launchpad-ui/inline-edit'; // plop end type exports // plop start module exports @@ -194,4 +195,5 @@ export { Tooltip, TooltipBase } from '@launchpad-ui/tooltip'; export { Stack } from '@launchpad-ui/stack'; export { Inline } from '@launchpad-ui/inline'; export { Column, Columns } from '@launchpad-ui/columns'; +export { InlineEdit } from '@launchpad-ui/inline-edit'; // plop end module exports diff --git a/packages/inline-edit/README.md b/packages/inline-edit/README.md new file mode 100644 index 000000000..ba5410abf --- /dev/null +++ b/packages/inline-edit/README.md @@ -0,0 +1,14 @@ +# @launchpad-ui/inline-edit + +> An element used to display and allow inline editing of a form element value. + +[![See it on NPM!](https://img.shields.io/npm/v/@launchpad-ui/inline-edit?style=for-the-badge)](https://www.npmjs.com/package/@launchpad-ui/inline-edit) +[![How big is this package in your project?](https://img.shields.io/bundlephobia/minzip/@launchpad-ui/inline-edit?style=for-the-badge)](https://bundlephobia.com/result?p=@launchpad-ui/inline-edit) + +## Installation + +```sh +$ yarn add @launchpad-ui/inline-edit +# or +$ npm install @launchpad-ui/inline-edit +``` diff --git a/packages/inline-edit/__tests__/InlineEdit.cy.tsx b/packages/inline-edit/__tests__/InlineEdit.cy.tsx new file mode 100644 index 000000000..0e8930b1a --- /dev/null +++ b/packages/inline-edit/__tests__/InlineEdit.cy.tsx @@ -0,0 +1,13 @@ +import { InlineEdit } from '../src'; + +describe('InlineEdit', () => { + it('renders', () => { + cy.mount(An important message); + cy.getByTestId('inline-edit').should('be.visible'); + }); + + it('is accessible', () => { + cy.mount(An important message); + cy.checkA11y(); + }); +}); diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx new file mode 100644 index 000000000..cdd66ae33 --- /dev/null +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -0,0 +1,11 @@ +import { it, expect, describe } from 'vitest'; + +import { render, screen } from '../../../test/utils'; +import { InlineEdit } from '../src'; + +describe('InlineEdit', () => { + it('renders', () => { + render(An important message); + expect(screen.getByText('An important message')).toBeInTheDocument(); + }); +}); diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json new file mode 100644 index 000000000..aa6060327 --- /dev/null +++ b/packages/inline-edit/package.json @@ -0,0 +1,47 @@ +{ + "name": "@launchpad-ui/inline-edit", + "version": "0.0.1", + "status": "alpha", + "publishConfig": { + "access": "public" + }, + "description": "An element used to display and allow inline editing of a form element value.", + "files": [ + "dist" + ], + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "dist/index.d.ts", + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.es.js", + "require": "./dist/index.js" + }, + "./package.json": "./package.json", + "./style.css": "./dist/style.css" + }, + "source": "src/index.ts", + "scripts": { + "build": "vite build -c ../../vite.config.ts && tsc --project tsconfig.build.json", + "clean": "rm -rf dist", + "lint": "eslint '**/*.{ts,tsx,js}' && stylelint '**/*.css' --ignore-path ../../.stylelintignore", + "test": "vitest run --coverage" + }, + "dependencies": { + "@launchpad-ui/tokens": "workspace:~", + "@launchpad-ui/vars": "workspace:~", + "classix": "2.1.17" + }, + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0" + } +} diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx new file mode 100644 index 000000000..74e11ee93 --- /dev/null +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -0,0 +1,38 @@ +import type { InlineVariants } from './styles/InlineEdit.css'; +import type { ComponentProps } from 'react'; + +import { ButtonGroup, IconButton } from '@launchpad-ui/button'; +import { TextField } from '@launchpad-ui/form'; +import { Icon } from '@launchpad-ui/icons'; +import { cx } from 'classix'; + +import { container, cancelButton, inline } from './styles/InlineEdit.css'; + +type InlineEditProps = ComponentProps<'div'> & + InlineVariants & { + 'data-test-id'?: string; + }; + +const InlineEdit = ({ + className, + 'data-test-id': testId = 'inline-edit', + layout = 'horizontal', +}: InlineEditProps) => { + return ( +
+ + + } aria-label="save" /> + } + aria-label="cancel" + className={cancelButton} + /> + +
+ ); +}; + +export { InlineEdit }; +export type { InlineEditProps }; diff --git a/packages/inline-edit/src/index.ts b/packages/inline-edit/src/index.ts new file mode 100644 index 000000000..2ffe4e57f --- /dev/null +++ b/packages/inline-edit/src/index.ts @@ -0,0 +1,2 @@ +export type { InlineEditProps } from './InlineEdit'; +export { InlineEdit } from './InlineEdit'; diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts new file mode 100644 index 000000000..46fa1760d --- /dev/null +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -0,0 +1,33 @@ +import type { RecipeVariants } from '@vanilla-extract/recipes'; + +import { vars } from '@launchpad-ui/vars'; +import { style } from '@vanilla-extract/css'; +import { recipe } from '@vanilla-extract/recipes'; + +const inline = recipe({ + variants: { + layout: { + vertical: { flexDirection: 'column' }, + horizontal: { flexDirection: 'row' }, + }, + }, +}); + +const container = style({ + display: 'flex', + gap: vars.spacing[300], +}); + +const cancelButton = style({ + selectors: { + '.Button--icon&': { + height: '3rem', + width: '3rem', + }, + }, +}); + +type InlineVariants = RecipeVariants; + +export { container, cancelButton, inline }; +export type { InlineVariants }; diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx new file mode 100644 index 000000000..44a07230e --- /dev/null +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -0,0 +1,20 @@ +import type { StoryObj } from '@storybook/react'; + +import { InlineEdit } from '../src'; + +export default { + component: InlineEdit, + title: 'Components/InlineEdit', + description: 'An element used to display and allow inline editing of a form element value.', + parameters: { + status: { + type: import.meta.env.STORYBOOK_PACKAGE_STATUS__INLINE_EDIT, + }, + }, +}; + +type Story = StoryObj; + +export const Example: Story = { + args: {}, +}; diff --git a/packages/inline-edit/tsconfig.build.json b/packages/inline-edit/tsconfig.build.json new file mode 100644 index 000000000..8f691eb77 --- /dev/null +++ b/packages/inline-edit/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/*.ts", "src/*.tsx", "../../types/declarations.d.ts"], + "compilerOptions": { + "outDir": "./dist" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f732e8445..f3e4035ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: '@vanilla-extract/css': specifier: ^1.12.0 version: 1.12.0 + '@vanilla-extract/recipes': + specifier: ^0.4.0 + version: 0.4.0(@vanilla-extract/css@1.12.0) '@vanilla-extract/vite-plugin': specifier: ^3.8.2 version: 3.8.2(@types/node@18.17.0)(ts-node@10.9.1)(vite@4.4.2) @@ -521,6 +524,9 @@ importers: '@launchpad-ui/inline': specifier: workspace:~ version: link:../inline + '@launchpad-ui/inline-edit': + specifier: workspace:~ + version: link:../inline-edit '@launchpad-ui/markdown': specifier: workspace:~ version: link:../markdown @@ -795,6 +801,25 @@ importers: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + packages/inline-edit: + dependencies: + '@launchpad-ui/tokens': + specifier: workspace:~ + version: link:../tokens + '@launchpad-ui/vars': + specifier: workspace:~ + version: link:../vars + classix: + specifier: 2.1.17 + version: 2.1.17 + devDependencies: + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + packages/markdown: dependencies: '@launchpad-ui/tokens': @@ -7912,6 +7937,14 @@ packages: resolution: {integrity: sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ==} dev: true + /@vanilla-extract/recipes@0.4.0(@vanilla-extract/css@1.12.0): + resolution: {integrity: sha512-gFgB7BofUYbtbxINHK6DhMv1JDFDXp/YI/Xm+cqKar+1I/2dfxPepeDxSexL6YB4ftfeaDw8Kn5zydMvHcGOEQ==} + peerDependencies: + '@vanilla-extract/css': ^1.0.0 + dependencies: + '@vanilla-extract/css': 1.12.0 + dev: true + /@vanilla-extract/vite-plugin@3.8.2(@types/node@18.17.0)(ts-node@10.9.1)(vite@4.4.2): resolution: {integrity: sha512-i0vpuBUoh10Obl0hJr0dWQa6M3Udu/irm4tnsg1lUze8DXTbv3ctHmVu/wrRZHKw1EzzW/v+nLoJJRvisApspQ==} peerDependencies: diff --git a/tsconfig.json b/tsconfig.json index 31175e86d..3d2b56018 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "@launchpad-ui/form": ["./packages/form/src"], "@launchpad-ui/icons": ["./packages/icons/src"], "@launchpad-ui/inline": ["./packages/inline/src"], + "@launchpad-ui/inline-edit": ["./packages/inline-edit/src"], "@launchpad-ui/markdown": ["./packages/markdown/src"], "@launchpad-ui/menu": ["./packages/menu/src"], "@launchpad-ui/modal": ["./packages/modal/src"], From df0865e0f2445d47214f6a783d89daded706acea Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 19 Jul 2023 09:41:44 -0400 Subject: [PATCH 02/26] feat: basic read and edit --- packages/inline-edit/package.json | 1 + packages/inline-edit/src/InlineEdit.tsx | 44 ++++++++++++++++--- .../inline-edit/src/styles/InlineEdit.css.ts | 1 + .../stories/InlineEdit.stories.tsx | 25 ++++++++++- pnpm-lock.yaml | 3 ++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json index aa6060327..94abce212 100644 --- a/packages/inline-edit/package.json +++ b/packages/inline-edit/package.json @@ -34,6 +34,7 @@ "dependencies": { "@launchpad-ui/tokens": "workspace:~", "@launchpad-ui/vars": "workspace:~", + "@radix-ui/react-slot": "^1.0.0", "classix": "2.1.17" }, "peerDependencies": { diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 74e11ee93..c24161c7d 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -1,36 +1,70 @@ import type { InlineVariants } from './styles/InlineEdit.css'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, Dispatch, SetStateAction } from 'react'; import { ButtonGroup, IconButton } from '@launchpad-ui/button'; import { TextField } from '@launchpad-ui/form'; import { Icon } from '@launchpad-ui/icons'; +import { Slot } from '@radix-ui/react-slot'; import { cx } from 'classix'; +import { useRef, useState } from 'react'; import { container, cancelButton, inline } from './styles/InlineEdit.css'; type InlineEditProps = ComponentProps<'div'> & - InlineVariants & { + InlineVariants & + Pick, 'defaultValue'> & { 'data-test-id'?: string; + onSave: Dispatch>; }; const InlineEdit = ({ className, 'data-test-id': testId = 'inline-edit', layout = 'horizontal', + children, + defaultValue, + onSave, }: InlineEditProps) => { - return ( + const [isEditing, setEditing] = useState(false); + const inputRef = useRef(null); + + const handleEdit = () => { + setEditing(true); + }; + + const handleCancel = () => { + setEditing(false); + }; + + const handleSave = () => { + onSave(inputRef.current?.value || ''); + setEditing(false); + }; + + return isEditing ? (
- + - } aria-label="save" /> + } + aria-label="save" + onClick={handleSave} + /> } aria-label="cancel" className={cancelButton} + onClick={handleCancel} />
+ ) : ( +
+ {children} + } aria-label="edit" size="small" onClick={handleEdit} /> +
); }; diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts index 46fa1760d..ea951135d 100644 --- a/packages/inline-edit/src/styles/InlineEdit.css.ts +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -16,6 +16,7 @@ const inline = recipe({ const container = style({ display: 'flex', gap: vars.spacing[300], + alignItems: 'center', }); const cancelButton = style({ diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index 44a07230e..d4144f60c 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -1,5 +1,8 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import type { StoryObj } from '@storybook/react'; +import { useState } from '@storybook/client-api'; + import { InlineEdit } from '../src'; export default { @@ -16,5 +19,25 @@ export default { type Story = StoryObj; export const Example: Story = { - args: {}, + render: (args) => { + const [editValue, setEditValue] = useState('edit me'); + + return ( + + {editValue} + + ); + }, +}; + +export const Title: Story = { + render: (args) => { + const [editValue, setEditValue] = useState('This is a title'); + + return ( + +

{editValue}

+
+ ); + }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3e4035ae..37fbc382a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -809,6 +809,9 @@ importers: '@launchpad-ui/vars': specifier: workspace:~ version: link:../vars + '@radix-ui/react-slot': + specifier: ^1.0.0 + version: 1.0.0(react@18.2.0) classix: specifier: 2.1.17 version: 2.1.17 From 97db03e9d0c9b15510f63dec4f46b69f55177959 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Thu, 20 Jul 2023 11:43:53 -0400 Subject: [PATCH 03/26] feat: focus management --- .storybook/styles.css | 1 + packages/inline-edit/package.json | 2 + packages/inline-edit/src/InlineEdit.tsx | 48 ++++++++++++++++--- .../inline-edit/src/styles/InlineEdit.css.ts | 14 +++--- .../stories/InlineEdit.stories.tsx | 25 ++++++++-- pnpm-lock.yaml | 6 +++ 6 files changed, 79 insertions(+), 17 deletions(-) diff --git a/.storybook/styles.css b/.storybook/styles.css index 9a63fce3b..64536d7bd 100644 --- a/.storybook/styles.css +++ b/.storybook/styles.css @@ -27,6 +27,7 @@ h1, h2, h3 { color: var(--lp-color-text-ui-primary-base); + margin: 0; } @font-face { diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json index 94abce212..baa6f41e6 100644 --- a/packages/inline-edit/package.json +++ b/packages/inline-edit/package.json @@ -32,6 +32,8 @@ "test": "vitest run --coverage" }, "dependencies": { + "@react-aria/focus": "3.13.0", + "@react-aria/utils": "3.18.0", "@launchpad-ui/tokens": "workspace:~", "@launchpad-ui/vars": "workspace:~", "@radix-ui/react-slot": "^1.0.0", diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index c24161c7d..737c541e5 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -1,10 +1,13 @@ import type { InlineVariants } from './styles/InlineEdit.css'; +import type { ButtonProps } from '@launchpad-ui/button'; import type { ComponentProps, Dispatch, SetStateAction } from 'react'; -import { ButtonGroup, IconButton } from '@launchpad-ui/button'; +import { Button, ButtonGroup, IconButton } from '@launchpad-ui/button'; import { TextField } from '@launchpad-ui/form'; import { Icon } from '@launchpad-ui/icons'; import { Slot } from '@radix-ui/react-slot'; +import { focusSafely } from '@react-aria/focus'; +import { useUpdateEffect } from '@react-aria/utils'; import { cx } from 'classix'; import { useRef, useState } from 'react'; @@ -15,18 +18,26 @@ type InlineEditProps = ComponentProps<'div'> & Pick, 'defaultValue'> & { 'data-test-id'?: string; onSave: Dispatch>; + hideEdit?: boolean; }; const InlineEdit = ({ - className, 'data-test-id': testId = 'inline-edit', layout = 'horizontal', children, defaultValue, onSave, + hideEdit = false, }: InlineEditProps) => { const [isEditing, setEditing] = useState(false); const inputRef = useRef(null); + const editRef = useRef(null); + + useUpdateEffect(() => { + isEditing + ? inputRef.current && focusSafely(inputRef.current) + : editRef.current && focusSafely(editRef.current); + }, [isEditing]); const handleEdit = () => { setEditing(true); @@ -41,9 +52,18 @@ const InlineEdit = ({ setEditing(false); }; + const ReadComponent = hideEdit ? Button : Slot; + const buttonProps: Partial = hideEdit + ? { + kind: 'minimal', + 'aria-label': 'edit', + style: { fontSize: 'inherit', fontWeight: 'inherit', lineHeight: 'inherit' }, + } + : {}; + return isEditing ? ( -
- +
+
) : ( -
- {children} - } aria-label="edit" size="small" onClick={handleEdit} /> +
+ undefined} + {...buttonProps} + > + {children} + + {!hideEdit && ( + } + aria-label="edit" + size="small" + onClick={handleEdit} + /> + )}
); }; diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts index ea951135d..8d43e9340 100644 --- a/packages/inline-edit/src/styles/InlineEdit.css.ts +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -4,21 +4,21 @@ import { vars } from '@launchpad-ui/vars'; import { style } from '@vanilla-extract/css'; import { recipe } from '@vanilla-extract/recipes'; +const container = style({ + display: 'flex', + gap: vars.spacing[300], + alignItems: 'center', +}); + const inline = recipe({ variants: { layout: { - vertical: { flexDirection: 'column' }, + vertical: { flexDirection: 'column', alignItems: 'flex-start' }, horizontal: { flexDirection: 'row' }, }, }, }); -const container = style({ - display: 'flex', - gap: vars.spacing[300], - alignItems: 'center', -}); - const cancelButton = style({ selectors: { '.Button--icon&': { diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index d4144f60c..1aabfc008 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -1,6 +1,7 @@ /* eslint-disable react-hooks/rules-of-hooks */ import type { StoryObj } from '@storybook/react'; +import { CopyToClipboard } from '@launchpad-ui/clipboard'; import { useState } from '@storybook/client-api'; import { InlineEdit } from '../src'; @@ -35,9 +36,27 @@ export const Title: Story = { const [editValue, setEditValue] = useState('This is a title'); return ( - -

{editValue}

-
+
+ +

{editValue}

+
+
+ ); + }, +}; + +export const Copy: Story = { + render: (args) => { + const [editValue, setEditValue] = useState('auto-generated-key'); + + return ( +
+ + + {editValue} + + +
); }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37fbc382a..573924279 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -812,6 +812,12 @@ importers: '@radix-ui/react-slot': specifier: ^1.0.0 version: 1.0.0(react@18.2.0) + '@react-aria/focus': + specifier: 3.13.0 + version: 3.13.0(react@18.2.0) + '@react-aria/utils': + specifier: 3.18.0 + version: 3.18.0(react@18.2.0) classix: specifier: 2.1.17 version: 2.1.17 From ec082e7c5e39a5f02528181ac153fa635d332439 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Thu, 20 Jul 2023 13:05:13 -0400 Subject: [PATCH 04/26] refactor: use class for text styles --- packages/inline-edit/src/InlineEdit.tsx | 4 ++-- packages/inline-edit/src/styles/InlineEdit.css.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 737c541e5..f447087c4 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -11,7 +11,7 @@ import { useUpdateEffect } from '@react-aria/utils'; import { cx } from 'classix'; import { useRef, useState } from 'react'; -import { container, cancelButton, inline } from './styles/InlineEdit.css'; +import { container, cancelButton, inline, buttonText } from './styles/InlineEdit.css'; type InlineEditProps = ComponentProps<'div'> & InlineVariants & @@ -57,7 +57,7 @@ const InlineEdit = ({ ? { kind: 'minimal', 'aria-label': 'edit', - style: { fontSize: 'inherit', fontWeight: 'inherit', lineHeight: 'inherit' }, + className: buttonText, } : {}; diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts index 8d43e9340..2c7674d98 100644 --- a/packages/inline-edit/src/styles/InlineEdit.css.ts +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -28,7 +28,13 @@ const cancelButton = style({ }, }); +const buttonText = style({ + fontSize: 'inherit', + fontWeight: 'inherit', + lineHeight: 'inherit', +}); + type InlineVariants = RecipeVariants; -export { container, cancelButton, inline }; +export { container, cancelButton, inline, buttonText }; export type { InlineVariants }; From ccf3dc5eb6f61a82b64cc8a8da87896f90a7f177 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Fri, 21 Jul 2023 08:56:19 -0400 Subject: [PATCH 05/26] feat: add key handler --- packages/inline-edit/src/InlineEdit.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index f447087c4..7a9d00ec5 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -1,6 +1,6 @@ import type { InlineVariants } from './styles/InlineEdit.css'; import type { ButtonProps } from '@launchpad-ui/button'; -import type { ComponentProps, Dispatch, SetStateAction } from 'react'; +import type { ComponentProps, Dispatch, KeyboardEventHandler, SetStateAction } from 'react'; import { Button, ButtonGroup, IconButton } from '@launchpad-ui/button'; import { TextField } from '@launchpad-ui/form'; @@ -52,6 +52,16 @@ const InlineEdit = ({ setEditing(false); }; + const handleKeyDown: KeyboardEventHandler = (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + handleSave(); + } else if (event.key === 'Escape') { + event.preventDefault(); + handleCancel(); + } + }; + const ReadComponent = hideEdit ? Button : Slot; const buttonProps: Partial = hideEdit ? { @@ -63,7 +73,7 @@ const InlineEdit = ({ return isEditing ? (
- + Date: Mon, 24 Jul 2023 09:53:11 -0400 Subject: [PATCH 06/26] feat: use RA button --- packages/inline-edit/package.json | 2 +- packages/inline-edit/src/InlineEdit.tsx | 60 ++++++++++--------- .../inline-edit/src/styles/InlineEdit.css.ts | 14 +++-- .../stories/InlineEdit.stories.tsx | 10 ++-- pnpm-lock.yaml | 6 +- 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json index baa6f41e6..46d4a971e 100644 --- a/packages/inline-edit/package.json +++ b/packages/inline-edit/package.json @@ -32,11 +32,11 @@ "test": "vitest run --coverage" }, "dependencies": { + "@react-aria/button": "3.8.0", "@react-aria/focus": "3.13.0", "@react-aria/utils": "3.18.0", "@launchpad-ui/tokens": "workspace:~", "@launchpad-ui/vars": "workspace:~", - "@radix-ui/react-slot": "^1.0.0", "classix": "2.1.17" }, "peerDependencies": { diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 7a9d00ec5..b946d91c4 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -1,17 +1,16 @@ import type { InlineVariants } from './styles/InlineEdit.css'; -import type { ButtonProps } from '@launchpad-ui/button'; import type { ComponentProps, Dispatch, KeyboardEventHandler, SetStateAction } from 'react'; -import { Button, ButtonGroup, IconButton } from '@launchpad-ui/button'; +import { ButtonGroup, IconButton } from '@launchpad-ui/button'; import { TextField } from '@launchpad-ui/form'; import { Icon } from '@launchpad-ui/icons'; -import { Slot } from '@radix-ui/react-slot'; +import { useButton } from '@react-aria/button'; import { focusSafely } from '@react-aria/focus'; import { useUpdateEffect } from '@react-aria/utils'; import { cx } from 'classix'; import { useRef, useState } from 'react'; -import { container, cancelButton, inline, buttonText } from './styles/InlineEdit.css'; +import { container, cancelButton, inline, readButton } from './styles/InlineEdit.css'; type InlineEditProps = ComponentProps<'div'> & InlineVariants & @@ -62,14 +61,32 @@ const InlineEdit = ({ } }; - const ReadComponent = hideEdit ? Button : Slot; - const buttonProps: Partial = hideEdit - ? { - kind: 'minimal', - 'aria-label': 'edit', - className: buttonText, - } - : {}; + const { buttonProps } = useButton( + { + 'aria-label': 'edit', + elementType: 'div', + onPress: handleEdit, + }, + editRef + ); + + const renderReadContent = () => + hideEdit ? ( + + {children} + + ) : ( + <> + {children} + } + aria-label="edit" + size="small" + onClick={handleEdit} + /> + + ); return isEditing ? (
@@ -91,23 +108,8 @@ const InlineEdit = ({
) : ( -
- undefined} - {...buttonProps} - > - {children} - - {!hideEdit && ( - } - aria-label="edit" - size="small" - onClick={handleEdit} - /> - )} +
+ {renderReadContent()}
); }; diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts index 2c7674d98..c7ee9f3e5 100644 --- a/packages/inline-edit/src/styles/InlineEdit.css.ts +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -28,13 +28,17 @@ const cancelButton = style({ }, }); -const buttonText = style({ - fontSize: 'inherit', - fontWeight: 'inherit', - lineHeight: 'inherit', +const readButton = style({ + display: 'block', + padding: `${vars.spacing[200]} ${vars.spacing[300]}`, + borderRadius: vars.border.radius.regular, + ':hover': { + background: vars.color.bg.interactive.tertiary.hover, + cursor: 'pointer', + }, }); type InlineVariants = RecipeVariants; -export { container, cancelButton, inline, buttonText }; +export { container, cancelButton, inline, readButton }; export type { InlineVariants }; diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index 1aabfc008..ac2a12cfb 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -24,9 +24,11 @@ export const Example: Story = { const [editValue, setEditValue] = useState('edit me'); return ( - - {editValue} - +
+ + {editValue} + +
); }, }; @@ -36,7 +38,7 @@ export const Title: Story = { const [editValue, setEditValue] = useState('This is a title'); return ( -
+

{editValue}

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 573924279..b166b9ced 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -809,9 +809,9 @@ importers: '@launchpad-ui/vars': specifier: workspace:~ version: link:../vars - '@radix-ui/react-slot': - specifier: ^1.0.0 - version: 1.0.0(react@18.2.0) + '@react-aria/button': + specifier: 3.8.0 + version: 3.8.0(react@18.2.0) '@react-aria/focus': specifier: 3.13.0 version: 3.13.0(react@18.2.0) From 72b0a12967514cbd94c49db4612a4bb6033d14fb Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 11:24:28 -0400 Subject: [PATCH 07/26] fix: use inline for read wrapper --- packages/inline-edit/src/styles/InlineEdit.css.ts | 2 +- packages/inline-edit/stories/InlineEdit.stories.tsx | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts index c7ee9f3e5..5314e058c 100644 --- a/packages/inline-edit/src/styles/InlineEdit.css.ts +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -29,7 +29,7 @@ const cancelButton = style({ }); const readButton = style({ - display: 'block', + display: 'inline-block', padding: `${vars.spacing[200]} ${vars.spacing[300]}`, borderRadius: vars.border.radius.regular, ':hover': { diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index ac2a12cfb..ffc6887c0 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -24,11 +24,9 @@ export const Example: Story = { const [editValue, setEditValue] = useState('edit me'); return ( -
- - {editValue} - -
+ + {editValue} + ); }, }; From d8d37129a7398cf04620bdf8c13cf6c1e33483b5 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 13:41:06 -0400 Subject: [PATCH 08/26] feat: enable use of textarea --- packages/inline-edit/src/InlineEdit.tsx | 56 ++++++++++++------- .../inline-edit/src/styles/InlineEdit.css.ts | 6 ++ .../stories/InlineEdit.stories.tsx | 13 +++++ 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index b946d91c4..0c6aa28c0 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -1,5 +1,12 @@ import type { InlineVariants } from './styles/InlineEdit.css'; -import type { ComponentProps, Dispatch, KeyboardEventHandler, SetStateAction } from 'react'; +import type { TextAreaProps, TextFieldProps } from '@launchpad-ui/form'; +import type { + ComponentProps, + Dispatch, + KeyboardEventHandler, + ReactElement, + SetStateAction, +} from 'react'; import { ButtonGroup, IconButton } from '@launchpad-ui/button'; import { TextField } from '@launchpad-ui/form'; @@ -8,7 +15,7 @@ import { useButton } from '@react-aria/button'; import { focusSafely } from '@react-aria/focus'; import { useUpdateEffect } from '@react-aria/utils'; import { cx } from 'classix'; -import { useRef, useState } from 'react'; +import { cloneElement, useRef, useState } from 'react'; import { container, cancelButton, inline, readButton } from './styles/InlineEdit.css'; @@ -18,6 +25,7 @@ type InlineEditProps = ComponentProps<'div'> & 'data-test-id'?: string; onSave: Dispatch>; hideEdit?: boolean; + input?: ReactElement; }; const InlineEdit = ({ @@ -27,6 +35,7 @@ const InlineEdit = ({ defaultValue, onSave, hideEdit = false, + input = , }: InlineEditProps) => { const [isEditing, setEditing] = useState(false); const inputRef = useRef(null); @@ -70,27 +79,32 @@ const InlineEdit = ({ editRef ); - const renderReadContent = () => - hideEdit ? ( - - {children} - - ) : ( - <> - {children} - } - aria-label="edit" - size="small" - onClick={handleEdit} - /> - - ); + const renderReadContent = hideEdit ? ( + + {children} + + ) : ( + <> + {children} + } + aria-label="edit" + size="small" + onClick={handleEdit} + /> + + ); + + const renderInput = cloneElement(input, { + ref: inputRef, + defaultValue, + onKeyDown: handleKeyDown, + }); return isEditing ? (
- + {renderInput} ) : (
- {renderReadContent()} + {renderReadContent}
); }; diff --git a/packages/inline-edit/src/styles/InlineEdit.css.ts b/packages/inline-edit/src/styles/InlineEdit.css.ts index 5314e058c..93eb01a15 100644 --- a/packages/inline-edit/src/styles/InlineEdit.css.ts +++ b/packages/inline-edit/src/styles/InlineEdit.css.ts @@ -36,6 +36,12 @@ const readButton = style({ background: vars.color.bg.interactive.tertiary.hover, cursor: 'pointer', }, + ':focus-visible': { + borderRadius: vars.border.radius.medium, + boxShadow: `0 0 0 2px ${vars.color.bg.ui.primary}, 0 0 0 4px ${vars.color.shadow.interactive.focus}`, + outline: 0, + zIndex: 2, + }, }); type InlineVariants = RecipeVariants; diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index ffc6887c0..ed0facef9 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -2,6 +2,7 @@ import type { StoryObj } from '@storybook/react'; import { CopyToClipboard } from '@launchpad-ui/clipboard'; +import { TextArea } from '@launchpad-ui/form'; import { useState } from '@storybook/client-api'; import { InlineEdit } from '../src'; @@ -60,3 +61,15 @@ export const Copy: Story = { ); }, }; + +export const Textarea: Story = { + render: (args) => { + const [editValue, setEditValue] = useState('edit me'); + + return ( + }> + {editValue} + + ); + }, +}; From adbeb9b651e387c6f411ba06823f183386bfb401 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 14:30:52 -0400 Subject: [PATCH 09/26] feat: add play for snapshot --- packages/inline-edit/stories/InlineEdit.stories.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index ed0facef9..a73bf980c 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -4,6 +4,7 @@ import type { StoryObj } from '@storybook/react'; import { CopyToClipboard } from '@launchpad-ui/clipboard'; import { TextArea } from '@launchpad-ui/form'; import { useState } from '@storybook/client-api'; +import { userEvent, within } from '@storybook/testing-library'; import { InlineEdit } from '../src'; @@ -30,6 +31,12 @@ export const Example: Story = { ); }, + play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { + const canvas = within(canvasElement); + + const edit = canvas.getAllByRole('button'); + userEvent.click(edit[0]); + }, }; export const Title: Story = { From c43735c1737d650d0f7c6147cd80add7c83637c3 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 16:18:16 -0400 Subject: [PATCH 10/26] feat: merge props for input --- packages/inline-edit/src/InlineEdit.tsx | 17 +++++--- .../stories/InlineEdit.stories.tsx | 39 ++++++++++++++++--- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 0c6aa28c0..044384c93 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -13,7 +13,7 @@ import { TextField } from '@launchpad-ui/form'; import { Icon } from '@launchpad-ui/icons'; import { useButton } from '@react-aria/button'; import { focusSafely } from '@react-aria/focus'; -import { useUpdateEffect } from '@react-aria/utils'; +import { mergeProps, useUpdateEffect } from '@react-aria/utils'; import { cx } from 'classix'; import { cloneElement, useRef, useState } from 'react'; @@ -36,6 +36,7 @@ const InlineEdit = ({ onSave, hideEdit = false, input = , + 'aria-label': ariaLabel, }: InlineEditProps) => { const [isEditing, setEditing] = useState(false); const inputRef = useRef(null); @@ -96,11 +97,15 @@ const InlineEdit = ({ ); - const renderInput = cloneElement(input, { - ref: inputRef, - defaultValue, - onKeyDown: handleKeyDown, - }); + const renderInput = cloneElement( + input, + mergeProps(input.props, { + ref: inputRef, + defaultValue, + onKeyDown: handleKeyDown, + 'aria-label': ariaLabel, + }) + ); return isEditing ? (
diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index a73bf980c..114524c9b 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/react'; import { CopyToClipboard } from '@launchpad-ui/clipboard'; -import { TextArea } from '@launchpad-ui/form'; +import { Form, FormField, TextArea, TextField } from '@launchpad-ui/form'; import { useState } from '@storybook/client-api'; import { userEvent, within } from '@storybook/testing-library'; @@ -39,9 +39,9 @@ export const Example: Story = { }, }; -export const Title: Story = { +export const EditTitle: Story = { render: (args) => { - const [editValue, setEditValue] = useState('This is a title'); + const [editValue, setEditValue] = useState('Unnamed title'); return (
@@ -53,7 +53,7 @@ export const Title: Story = { }, }; -export const Copy: Story = { +export const EditCopy: Story = { render: (args) => { const [editValue, setEditValue] = useState('auto-generated-key'); @@ -69,9 +69,9 @@ export const Copy: Story = { }, }; -export const Textarea: Story = { +export const WithTextarea: Story = { render: (args) => { - const [editValue, setEditValue] = useState('edit me'); + const [editValue, setEditValue] = useState('edit description'); return ( }> @@ -80,3 +80,30 @@ export const Textarea: Story = { ); }, }; + +export const InForm: Story = { + render: (args) => { + const [editValue, setEditValue] = useState(''); + + return ( +
+ + } + > + {editValue || 'Enter a value'} + + +
+ ); + }, +}; From dac3053ae3c10657773beb687f998c2cec66cb6c Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 16:29:39 -0400 Subject: [PATCH 11/26] feat: add basic tests --- .../remix/app/routes/components.inline-edit.tsx | 8 +++++++- .../inline-edit/__tests__/InlineEdit.cy.tsx | 16 ++++++++++++++-- .../inline-edit/__tests__/InlineEdit.spec.tsx | 17 +++++++++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/remix/app/routes/components.inline-edit.tsx b/apps/remix/app/routes/components.inline-edit.tsx index 14b6fca01..f2062fae7 100644 --- a/apps/remix/app/routes/components.inline-edit.tsx +++ b/apps/remix/app/routes/components.inline-edit.tsx @@ -1,5 +1,11 @@ import { InlineEdit } from '@launchpad-ui/core'; +import { useState } from 'react'; export default function Index() { - return A lovely InlineEdit component.; + const [editValue, setEditValue] = useState('edit me'); + return ( + + {editValue} + + ); } diff --git a/packages/inline-edit/__tests__/InlineEdit.cy.tsx b/packages/inline-edit/__tests__/InlineEdit.cy.tsx index 0e8930b1a..834fbe9ba 100644 --- a/packages/inline-edit/__tests__/InlineEdit.cy.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.cy.tsx @@ -2,12 +2,24 @@ import { InlineEdit } from '../src'; describe('InlineEdit', () => { it('renders', () => { - cy.mount(An important message); + const editValue = 'test'; + cy.mount( + undefined}> + {editValue} + + ); cy.getByTestId('inline-edit').should('be.visible'); }); it('is accessible', () => { - cy.mount(An important message); + const editValue = 'test'; + cy.mount( + undefined}> + {editValue} + + ); + cy.checkA11y(); + cy.getByRole('button').click(); cy.checkA11y(); }); }); diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index cdd66ae33..9503b1328 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -1,11 +1,24 @@ +import type { InlineEditProps } from '../src'; + +import { useState } from 'react'; import { it, expect, describe } from 'vitest'; import { render, screen } from '../../../test/utils'; import { InlineEdit } from '../src'; +const InlineEditComponent = ({ ...props }: Partial) => { + const [editValue, setEditValue] = useState('test'); + + return ( + + {editValue} + + ); +}; + describe('InlineEdit', () => { it('renders', () => { - render(An important message); - expect(screen.getByText('An important message')).toBeInTheDocument(); + render(); + expect(screen.getByTestId('inline-edit')).toBeInTheDocument(); }); }); From e91ec6f62cc8e2381a8ffe9a446014b89e6315c0 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 17:03:01 -0400 Subject: [PATCH 12/26] fix: add missing dependencies --- packages/inline-edit/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json index 46d4a971e..0b320d7ae 100644 --- a/packages/inline-edit/package.json +++ b/packages/inline-edit/package.json @@ -28,13 +28,16 @@ "scripts": { "build": "vite build -c ../../vite.config.ts && tsc --project tsconfig.build.json", "clean": "rm -rf dist", - "lint": "eslint '**/*.{ts,tsx,js}' && stylelint '**/*.css' --ignore-path ../../.stylelintignore", + "lint": "eslint '**/*.{ts,tsx,js}'", "test": "vitest run --coverage" }, "dependencies": { "@react-aria/button": "3.8.0", "@react-aria/focus": "3.13.0", "@react-aria/utils": "3.18.0", + "@launchpad-ui/button": "workspace:~", + "@launchpad-ui/form": "workspace:~", + "@launchpad-ui/icons": "workspace:~", "@launchpad-ui/tokens": "workspace:~", "@launchpad-ui/vars": "workspace:~", "classix": "2.1.17" From 3fecbc0fcc3edb0989d3d8d394afafe5f087ed90 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Mon, 24 Jul 2023 17:14:04 -0400 Subject: [PATCH 13/26] fix: update lockfile --- pnpm-lock.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b166b9ced..c45704904 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -803,6 +803,15 @@ importers: packages/inline-edit: dependencies: + '@launchpad-ui/button': + specifier: workspace:~ + version: link:../button + '@launchpad-ui/form': + specifier: workspace:~ + version: link:../form + '@launchpad-ui/icons': + specifier: workspace:~ + version: link:../icons '@launchpad-ui/tokens': specifier: workspace:~ version: link:../tokens From e4de509a6856fad8d740f7d505008d5bed44aca7 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Tue, 25 Jul 2023 10:32:20 -0400 Subject: [PATCH 14/26] feat: add more tests --- .../inline-edit/__tests__/InlineEdit.cy.tsx | 4 +- .../inline-edit/__tests__/InlineEdit.spec.tsx | 124 +++++++++++++++++- packages/inline-edit/src/InlineEdit.tsx | 2 +- 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/packages/inline-edit/__tests__/InlineEdit.cy.tsx b/packages/inline-edit/__tests__/InlineEdit.cy.tsx index 834fbe9ba..feda04465 100644 --- a/packages/inline-edit/__tests__/InlineEdit.cy.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.cy.tsx @@ -14,12 +14,12 @@ describe('InlineEdit', () => { it('is accessible', () => { const editValue = 'test'; cy.mount( - undefined}> + undefined} aria-label="edit value"> {editValue} ); cy.checkA11y(); - cy.getByRole('button').click(); + cy.getByTestId('icon-button').click(); cy.checkA11y(); }); }); diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index 9503b1328..ed0d19156 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -1,13 +1,14 @@ import type { InlineEditProps } from '../src'; +import { TextArea } from '@launchpad-ui/form'; import { useState } from 'react'; import { it, expect, describe } from 'vitest'; -import { render, screen } from '../../../test/utils'; +import { render, screen, waitFor, userEvent } from '../../../test/utils'; import { InlineEdit } from '../src'; const InlineEditComponent = ({ ...props }: Partial) => { - const [editValue, setEditValue] = useState('test'); + const [editValue, setEditValue] = useState(''); return ( @@ -19,6 +20,123 @@ const InlineEditComponent = ({ ...props }: Partial) => { describe('InlineEdit', () => { it('renders', () => { render(); - expect(screen.getByTestId('inline-edit')).toBeInTheDocument(); + expect(screen.getByTestId('inline-edit')).toBeVisible(); + }); + + it('renders an input in edit mode', async () => { + render(); + screen.getByLabelText('edit').click(); + + await waitFor(() => { + expect(screen.getByTestId('text-field')).toBeVisible(); + }); + }); + + it('renders a button wrapper in edit mode when hideEdit is passed', async () => { + render(); + expect(screen.getByRole('button')).toBeVisible(); + }); + + it('renders a custom input', async () => { + render(} />); + screen.getByLabelText('edit').click(); + + await waitFor(() => { + expect(screen.getByTestId('text-area')).toBeVisible(); + }); + }); + + it('enters edit mode when button wrapper is clicked', async () => { + render(); + screen.getByRole('button').click(); + + await waitFor(() => { + expect(screen.getByTestId('text-field')).toBeVisible(); + }); + }); + + it('returns to read mode when cancel is clicked', async () => { + render(); + screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('cancel').click(); + }); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + }); + + it('returns to read mode when the escape key is pressed', async () => { + const user = userEvent.setup(); + render(); + screen.getByLabelText('edit').click(); + + await waitFor(async () => { + expect(screen.getByTestId('text-field')).toHaveFocus(); + await user.keyboard('{Escape}'); + }); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + }); + + it('saves the value and returns to read mode when the enter key is pressed', async () => { + const user = userEvent.setup(); + render(); + screen.getByLabelText('edit').click(); + + await waitFor(async () => { + expect(screen.getByTestId('text-field')).toHaveFocus(); + await user.keyboard('test'); + await user.keyboard('{Enter}'); + }); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + + await waitFor(() => { + expect(screen.getByText('test')).toBeVisible(); + }); + }); + + it('saves the value and returns to read mode when save is clicked', async () => { + const user = userEvent.setup(); + render(); + screen.getByLabelText('edit').click(); + + await waitFor(async () => { + expect(screen.getByTestId('text-field')).toHaveFocus(); + await user.keyboard('test'); + screen.getByLabelText('save').click(); + }); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + + await waitFor(() => { + expect(screen.getByText('test')).toBeVisible(); + }); + }); + + it('delegates focus with keyboard nav', async () => { + const user = userEvent.setup(); + render(); + await user.tab(); + await user.keyboard('{Enter}'); + + await waitFor(async () => { + expect(screen.getByTestId('text-field')).toHaveFocus(); + await user.tab(); + await user.keyboard('{Enter}'); + }); + + await waitFor(async () => { + expect(screen.getByLabelText('edit')).toHaveFocus(); + }); }); }); diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 044384c93..26bb8a4b5 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -74,7 +74,7 @@ const InlineEdit = ({ const { buttonProps } = useButton( { 'aria-label': 'edit', - elementType: 'div', + elementType: 'span', onPress: handleEdit, }, editRef From 426221562804ac0b25832b0bcd3573a2061bccb2 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Tue, 25 Jul 2023 10:34:26 -0400 Subject: [PATCH 15/26] chore: add changeset --- .changeset/chatty-tables-attend.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/chatty-tables-attend.md diff --git a/.changeset/chatty-tables-attend.md b/.changeset/chatty-tables-attend.md new file mode 100644 index 000000000..05b4a147f --- /dev/null +++ b/.changeset/chatty-tables-attend.md @@ -0,0 +1,12 @@ +--- +'@launchpad-ui/inline-edit': minor +'@launchpad-ui/core': patch +--- + +Add `inline-edit` package to display and allow inline editing of a form elements: + +- Use props `defaultValue` and `onSave` to handle state management of the value to edit +- Have children act as the "read" view of the component +- Hide edit icon button and wrap children with a React Aria button when `hideEdit` is true +- Implement focus management to ensure focus is directed correctly when toggling between read and edit mode +- Use `input` prop to allow passing a custom `TextField` or `TextArea` component From e502a8a78a51debf8f774c3c7ac8cbe68e5a115f Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Tue, 25 Jul 2023 15:23:01 -0400 Subject: [PATCH 16/26] refactor: input -> renderInput --- .changeset/chatty-tables-attend.md | 2 +- packages/inline-edit/__tests__/InlineEdit.spec.tsx | 2 +- packages/inline-edit/src/InlineEdit.tsx | 12 ++++++------ packages/inline-edit/stories/InlineEdit.stories.tsx | 9 +++++++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.changeset/chatty-tables-attend.md b/.changeset/chatty-tables-attend.md index 05b4a147f..da483f83e 100644 --- a/.changeset/chatty-tables-attend.md +++ b/.changeset/chatty-tables-attend.md @@ -9,4 +9,4 @@ Add `inline-edit` package to display and allow inline editing of a form elements - Have children act as the "read" view of the component - Hide edit icon button and wrap children with a React Aria button when `hideEdit` is true - Implement focus management to ensure focus is directed correctly when toggling between read and edit mode -- Use `input` prop to allow passing a custom `TextField` or `TextArea` component +- Use `renderInput` prop to allow passing a custom `TextField` or `TextArea` component diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index ed0d19156..165a9f6a6 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -38,7 +38,7 @@ describe('InlineEdit', () => { }); it('renders a custom input', async () => { - render(} />); + render(} />); screen.getByLabelText('edit').click(); await waitFor(() => { diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 26bb8a4b5..b8acf0c9b 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -25,7 +25,7 @@ type InlineEditProps = ComponentProps<'div'> & 'data-test-id'?: string; onSave: Dispatch>; hideEdit?: boolean; - input?: ReactElement; + renderInput?: ReactElement; }; const InlineEdit = ({ @@ -35,7 +35,7 @@ const InlineEdit = ({ defaultValue, onSave, hideEdit = false, - input = , + renderInput = , 'aria-label': ariaLabel, }: InlineEditProps) => { const [isEditing, setEditing] = useState(false); @@ -97,9 +97,9 @@ const InlineEdit = ({ ); - const renderInput = cloneElement( - input, - mergeProps(input.props, { + const input = cloneElement( + renderInput, + mergeProps(renderInput.props, { ref: inputRef, defaultValue, onKeyDown: handleKeyDown, @@ -109,7 +109,7 @@ const InlineEdit = ({ return isEditing ? (
- {renderInput} + {input} }> + } + > {editValue} ); @@ -98,7 +103,7 @@ export const InForm: Story = { defaultValue={editValue} {...args} onSave={setEditValue} - input={} + renderInput={} > {editValue || 'Enter a value'} From f9b930320845fdbf7a88af090b88b70c09286318 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Tue, 25 Jul 2023 15:49:47 -0400 Subject: [PATCH 17/26] chore: bump recipes --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ebcbca352..07efa2efa 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/parser": "^6.1.0", "@vanilla-extract/css": "^1.12.0", - "@vanilla-extract/recipes": "^0.4.0", + "@vanilla-extract/recipes": "^0.5.0", "@vanilla-extract/vite-plugin": "^3.8.2", "@vitejs/plugin-react-swc": "^3.3.1", "@vitest/coverage-v8": "^0.33.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c45704904..b087b6b6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,8 +108,8 @@ importers: specifier: ^1.12.0 version: 1.12.0 '@vanilla-extract/recipes': - specifier: ^0.4.0 - version: 0.4.0(@vanilla-extract/css@1.12.0) + specifier: ^0.5.0 + version: 0.5.0(@vanilla-extract/css@1.12.0) '@vanilla-extract/vite-plugin': specifier: ^3.8.2 version: 3.8.2(@types/node@18.17.0)(ts-node@10.9.1)(vite@4.4.2) @@ -7955,8 +7955,8 @@ packages: resolution: {integrity: sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ==} dev: true - /@vanilla-extract/recipes@0.4.0(@vanilla-extract/css@1.12.0): - resolution: {integrity: sha512-gFgB7BofUYbtbxINHK6DhMv1JDFDXp/YI/Xm+cqKar+1I/2dfxPepeDxSexL6YB4ftfeaDw8Kn5zydMvHcGOEQ==} + /@vanilla-extract/recipes@0.5.0(@vanilla-extract/css@1.12.0): + resolution: {integrity: sha512-NfdZ8XyqCDG2RsO3FmYPALDMKg5045dRD97NbBb0Fog3LMDVXZxYgDOct5FAWob8U6W4GbhVpRZt1X9hNnH6fA==} peerDependencies: '@vanilla-extract/css': ^1.0.0 dependencies: From acdd668abb3099b5d7a8608cf15083545947c677 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 10:13:09 -0400 Subject: [PATCH 18/26] fix: update deps for vanilla-extract --- packages/inline-edit/package.json | 2 ++ pnpm-lock.yaml | 23 ++++++----------------- vite.config.ts | 1 + 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json index 0b320d7ae..a1ba73989 100644 --- a/packages/inline-edit/package.json +++ b/packages/inline-edit/package.json @@ -40,9 +40,11 @@ "@launchpad-ui/icons": "workspace:~", "@launchpad-ui/tokens": "workspace:~", "@launchpad-ui/vars": "workspace:~", + "@vanilla-extract/recipes": "^0.5.0", "classix": "2.1.17" }, "peerDependencies": { + "@vanilla-extract/css": "^1.12.0", "react": "18.2.0", "react-dom": "18.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b087b6b6d..eb5ed5835 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -827,6 +827,12 @@ importers: '@react-aria/utils': specifier: 3.18.0 version: 3.18.0(react@18.2.0) + '@vanilla-extract/css': + specifier: ^1.12.0 + version: 1.12.0 + '@vanilla-extract/recipes': + specifier: ^0.5.0 + version: 0.5.0(@vanilla-extract/css@1.12.0) classix: specifier: 2.1.17 version: 2.1.17 @@ -3794,7 +3800,6 @@ packages: /@emotion/hash@0.9.1: resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} - dev: true /@emotion/is-prop-valid@0.8.8: resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} @@ -7922,7 +7927,6 @@ packages: deepmerge: 4.3.1 media-query-parser: 2.0.2 outdent: 0.8.0 - dev: true /@vanilla-extract/integration@6.2.1(@types/node@18.17.0): resolution: {integrity: sha512-+xYJz07G7TFAMZGrOqArOsURG+xcYvqctujEkANjw2McCBvGEK505RxQqOuNiA9Mi9hgGdNp2JedSa94f3eoLg==} @@ -7953,7 +7957,6 @@ packages: /@vanilla-extract/private@1.0.3: resolution: {integrity: sha512-17kVyLq3ePTKOkveHxXuIJZtGYs+cSoev7BlP+Lf4916qfDhk/HBjvlYDe8egrea7LNPHKwSZJK/bzZC+Q6AwQ==} - dev: true /@vanilla-extract/recipes@0.5.0(@vanilla-extract/css@1.12.0): resolution: {integrity: sha512-NfdZ8XyqCDG2RsO3FmYPALDMKg5045dRD97NbBb0Fog3LMDVXZxYgDOct5FAWob8U6W4GbhVpRZt1X9hNnH6fA==} @@ -7961,7 +7964,6 @@ packages: '@vanilla-extract/css': ^1.0.0 dependencies: '@vanilla-extract/css': 1.12.0 - dev: true /@vanilla-extract/vite-plugin@3.8.2(@types/node@18.17.0)(ts-node@10.9.1)(vite@4.4.2): resolution: {integrity: sha512-i0vpuBUoh10Obl0hJr0dWQa6M3Udu/irm4tnsg1lUze8DXTbv3ctHmVu/wrRZHKw1EzzW/v+nLoJJRvisApspQ==} @@ -8351,7 +8353,6 @@ packages: /ahocorasick@1.0.2: resolution: {integrity: sha512-hCOfMzbFx5IDutmWLAt6MZwOUjIfSM9G9FyVxytmE4Rs/5YDPWQrD/+IR1w+FweD9H2oOZEnv36TmkjhNURBVA==} - dev: true /ajv-formats@2.1.1(ajv@8.12.0): resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} @@ -8433,7 +8434,6 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -9153,7 +9153,6 @@ packages: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true /chalk@5.2.0: resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==} @@ -9363,7 +9362,6 @@ packages: engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} @@ -9371,7 +9369,6 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true /colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -9665,7 +9662,6 @@ packages: /css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} - dev: true /css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -9679,7 +9675,6 @@ packages: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: true /cssstyle@3.0.0: resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} @@ -9689,7 +9684,6 @@ packages: /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - dev: true /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -9939,7 +9933,6 @@ packages: /deep-object-diff@1.1.9: resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} - dev: true /deepmerge-ts@4.3.0: resolution: {integrity: sha512-if3ZYdkD2dClhnXR5reKtG98cwyaRT1NeugQoAPTTfsOpV9kqyeiBF9Qa5RHjemb3KzD5ulqygv6ED3t5j9eJw==} @@ -9949,7 +9942,6 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - dev: true /default-browser-id@3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} @@ -13900,7 +13892,6 @@ packages: resolution: {integrity: sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==} dependencies: '@babel/runtime': 7.22.3 - dev: true /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} @@ -14931,7 +14922,6 @@ packages: /outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} - dev: true /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} @@ -17484,7 +17474,6 @@ packages: engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: true /supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} diff --git a/vite.config.ts b/vite.config.ts index 9923875e2..517e198aa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -56,6 +56,7 @@ export default defineConfig({ ...Object.keys(packageJSON.dependencies || {}), ...Object.keys(packageJSON.peerDependencies || {}), 'react/jsx-runtime', + '@vanilla-extract/recipes/createRuntimeFn', ], }, sourcemap: true, From f773bcc28c38cfa3037afece25fc143a0370789f Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 10:14:03 -0400 Subject: [PATCH 19/26] feat: add controlled mode --- .../inline-edit/__tests__/InlineEdit.spec.tsx | 45 ++++++++++++++++++- packages/inline-edit/src/InlineEdit.tsx | 23 ++++++++-- .../stories/InlineEdit.stories.tsx | 23 ++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index 165a9f6a6..81aae6235 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -2,7 +2,7 @@ import type { InlineEditProps } from '../src'; import { TextArea } from '@launchpad-ui/form'; import { useState } from 'react'; -import { it, expect, describe } from 'vitest'; +import { it, expect, describe, vi } from 'vitest'; import { render, screen, waitFor, userEvent } from '../../../test/utils'; import { InlineEdit } from '../src'; @@ -11,7 +11,7 @@ const InlineEditComponent = ({ ...props }: Partial) => { const [editValue, setEditValue] = useState(''); return ( - + {editValue} ); @@ -139,4 +139,45 @@ describe('InlineEdit', () => { expect(screen.getByLabelText('edit')).toHaveFocus(); }); }); + + it('calls handlers for edit, cancel, and save', async () => { + const editSpy = vi.fn(); + const cancelSpy = vi.fn(); + const saveSpy = vi.fn(); + + render(); + + screen.getByLabelText('edit').click(); + expect(editSpy).toHaveBeenCalledTimes(1); + + await waitFor(async () => { + screen.getByLabelText('cancel').click(); + }); + + expect(cancelSpy).toHaveBeenCalledTimes(1); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); + + await waitFor(() => { + screen.getByLabelText('save').click(); + }); + + expect(saveSpy).toHaveBeenCalledTimes(1); + }); + + it('allows control over the read and edit modes', async () => { + const { rerender } = render(); + + await waitFor(() => { + expect(screen.getByTestId('text-field')).toBeVisible(); + }); + + rerender(); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + }); }); diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index b8acf0c9b..a5d534244 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -26,6 +26,9 @@ type InlineEditProps = ComponentProps<'div'> & onSave: Dispatch>; hideEdit?: boolean; renderInput?: ReactElement; + isEditing?: boolean; + onCancel?: () => void; + onEdit?: () => void; }; const InlineEdit = ({ @@ -37,10 +40,20 @@ const InlineEdit = ({ hideEdit = false, renderInput = , 'aria-label': ariaLabel, + isEditing: isEditingProp, + onCancel, + onEdit, }: InlineEditProps) => { - const [isEditing, setEditing] = useState(false); + const [isEditing, setEditing] = useState(isEditingProp ?? false); const inputRef = useRef(null); const editRef = useRef(null); + const controlled = isEditingProp !== undefined; + + useUpdateEffect(() => { + if (controlled) { + setEditing(isEditingProp); + } + }, [isEditingProp]); useUpdateEffect(() => { isEditing @@ -49,16 +62,18 @@ const InlineEdit = ({ }, [isEditing]); const handleEdit = () => { - setEditing(true); + !controlled && setEditing(true); + onEdit?.(); }; const handleCancel = () => { - setEditing(false); + !controlled && setEditing(false); + onCancel?.(); }; const handleSave = () => { onSave(inputRef.current?.value || ''); - setEditing(false); + !controlled && setEditing(false); }; const handleKeyDown: KeyboardEventHandler = (event) => { diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index 6b739a605..cb60ff02d 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -112,3 +112,26 @@ export const InForm: Story = { ); }, }; + +export const Controlled: Story = { + render: (args) => { + const [editValue, setEditValue] = useState('edit me'); + const [isEditing, setEditing] = useState(true); + + return ( + setEditing(false)} + onEdit={() => setEditing(true)} + {...args} + onSave={(value) => { + setEditValue(value); + setEditing(false); + }} + > + {editValue} + + ); + }, +}; From 33c40b3fe9809bed21fe1bd9da83a1198c451e5a Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 10:32:20 -0400 Subject: [PATCH 20/26] fix: address warnings --- .../inline-edit/__tests__/InlineEdit.spec.tsx | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index 81aae6235..e182d5f62 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -25,7 +25,10 @@ describe('InlineEdit', () => { it('renders an input in edit mode', async () => { render(); - screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); await waitFor(() => { expect(screen.getByTestId('text-field')).toBeVisible(); @@ -39,7 +42,10 @@ describe('InlineEdit', () => { it('renders a custom input', async () => { render(} />); - screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); await waitFor(() => { expect(screen.getByTestId('text-area')).toBeVisible(); @@ -48,7 +54,10 @@ describe('InlineEdit', () => { it('enters edit mode when button wrapper is clicked', async () => { render(); - screen.getByRole('button').click(); + + await waitFor(() => { + screen.getByRole('button').click(); + }); await waitFor(() => { expect(screen.getByTestId('text-field')).toBeVisible(); @@ -57,7 +66,10 @@ describe('InlineEdit', () => { it('returns to read mode when cancel is clicked', async () => { render(); - screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); await waitFor(() => { screen.getByLabelText('cancel').click(); @@ -71,7 +83,10 @@ describe('InlineEdit', () => { it('returns to read mode when the escape key is pressed', async () => { const user = userEvent.setup(); render(); - screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); @@ -86,7 +101,10 @@ describe('InlineEdit', () => { it('saves the value and returns to read mode when the enter key is pressed', async () => { const user = userEvent.setup(); render(); - screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); @@ -106,7 +124,10 @@ describe('InlineEdit', () => { it('saves the value and returns to read mode when save is clicked', async () => { const user = userEvent.setup(); render(); - screen.getByLabelText('edit').click(); + + await waitFor(() => { + screen.getByLabelText('edit').click(); + }); await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); @@ -147,7 +168,10 @@ describe('InlineEdit', () => { render(); - screen.getByLabelText('edit').click(); + await waitFor(async () => { + screen.getByLabelText('edit').click(); + }); + expect(editSpy).toHaveBeenCalledTimes(1); await waitFor(async () => { From a670f376c9761b49be61a1fff307593c3e32dcc5 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 11:06:16 -0400 Subject: [PATCH 21/26] refactor: final tweaks --- .changeset/chatty-tables-attend.md | 5 +++- .../app/routes/components.inline-edit.tsx | 2 +- .../inline-edit/__tests__/InlineEdit.spec.tsx | 18 ++++++------- packages/inline-edit/src/InlineEdit.tsx | 26 ++++++++++++------- .../stories/InlineEdit.stories.tsx | 12 ++++----- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.changeset/chatty-tables-attend.md b/.changeset/chatty-tables-attend.md index da483f83e..a68c496f7 100644 --- a/.changeset/chatty-tables-attend.md +++ b/.changeset/chatty-tables-attend.md @@ -5,8 +5,11 @@ Add `inline-edit` package to display and allow inline editing of a form elements: -- Use props `defaultValue` and `onSave` to handle state management of the value to edit +- Use props `defaultValue` and `onConfirm` to handle state management of the value to edit - Have children act as the "read" view of the component - Hide edit icon button and wrap children with a React Aria button when `hideEdit` is true - Implement focus management to ensure focus is directed correctly when toggling between read and edit mode - Use `renderInput` prop to allow passing a custom `TextField` or `TextArea` component +- Add handlers for edit, cancel, and confirm actions +- Use prop `isEditing` to allow full control over the read and edit modes +- Add `@vanilla-extract/css` as a peer dependency for prop `layout` variant types diff --git a/apps/remix/app/routes/components.inline-edit.tsx b/apps/remix/app/routes/components.inline-edit.tsx index f2062fae7..2def0ce2f 100644 --- a/apps/remix/app/routes/components.inline-edit.tsx +++ b/apps/remix/app/routes/components.inline-edit.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; export default function Index() { const [editValue, setEditValue] = useState('edit me'); return ( - + {editValue} ); diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index e182d5f62..46cccce80 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -11,7 +11,7 @@ const InlineEditComponent = ({ ...props }: Partial) => { const [editValue, setEditValue] = useState(''); return ( - + {editValue} ); @@ -98,7 +98,7 @@ describe('InlineEdit', () => { }); }); - it('saves the value and returns to read mode when the enter key is pressed', async () => { + it('confirms the value and returns to read mode when the enter key is pressed', async () => { const user = userEvent.setup(); render(); @@ -121,7 +121,7 @@ describe('InlineEdit', () => { }); }); - it('saves the value and returns to read mode when save is clicked', async () => { + it('confirms the value and returns to read mode when confirm is clicked', async () => { const user = userEvent.setup(); render(); @@ -132,7 +132,7 @@ describe('InlineEdit', () => { await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); await user.keyboard('test'); - screen.getByLabelText('save').click(); + screen.getByLabelText('confirm').click(); }); await waitFor(() => { @@ -161,12 +161,12 @@ describe('InlineEdit', () => { }); }); - it('calls handlers for edit, cancel, and save', async () => { + it('calls handlers for edit, cancel, and confirm', async () => { const editSpy = vi.fn(); const cancelSpy = vi.fn(); - const saveSpy = vi.fn(); + const confirmSpy = vi.fn(); - render(); + render(); await waitFor(async () => { screen.getByLabelText('edit').click(); @@ -185,10 +185,10 @@ describe('InlineEdit', () => { }); await waitFor(() => { - screen.getByLabelText('save').click(); + screen.getByLabelText('confirm').click(); }); - expect(saveSpy).toHaveBeenCalledTimes(1); + expect(confirmSpy).toHaveBeenCalledTimes(1); }); it('allows control over the read and edit modes', async () => { diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index a5d534244..391732027 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -23,12 +23,15 @@ type InlineEditProps = ComponentProps<'div'> & InlineVariants & Pick, 'defaultValue'> & { 'data-test-id'?: string; - onSave: Dispatch>; + onConfirm: Dispatch>; hideEdit?: boolean; renderInput?: ReactElement; isEditing?: boolean; onCancel?: () => void; onEdit?: () => void; + cancelButtonLabel?: string; + editButtonLabel?: string; + confirmButtonLabel?: string; }; const InlineEdit = ({ @@ -36,13 +39,16 @@ const InlineEdit = ({ layout = 'horizontal', children, defaultValue, - onSave, + onConfirm, hideEdit = false, renderInput = , 'aria-label': ariaLabel, isEditing: isEditingProp, onCancel, onEdit, + cancelButtonLabel = 'cancel', + editButtonLabel = 'edit', + confirmButtonLabel = 'confirm', }: InlineEditProps) => { const [isEditing, setEditing] = useState(isEditingProp ?? false); const inputRef = useRef(null); @@ -71,15 +77,15 @@ const InlineEdit = ({ onCancel?.(); }; - const handleSave = () => { - onSave(inputRef.current?.value || ''); + const handleConfirm = () => { + onConfirm(inputRef.current?.value || ''); !controlled && setEditing(false); }; const handleKeyDown: KeyboardEventHandler = (event) => { if (event.key === 'Enter') { event.preventDefault(); - handleSave(); + handleConfirm(); } else if (event.key === 'Escape') { event.preventDefault(); handleCancel(); @@ -88,7 +94,7 @@ const InlineEdit = ({ const { buttonProps } = useButton( { - 'aria-label': 'edit', + 'aria-label': editButtonLabel, elementType: 'span', onPress: handleEdit, }, @@ -105,7 +111,7 @@ const InlineEdit = ({ } - aria-label="edit" + aria-label={editButtonLabel} size="small" onClick={handleEdit} /> @@ -129,13 +135,13 @@ const InlineEdit = ({ } - aria-label="save" - onClick={handleSave} + aria-label={confirmButtonLabel} + onClick={handleConfirm} /> } - aria-label="cancel" + aria-label={cancelButtonLabel} className={cancelButton} onClick={handleCancel} /> diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index cb60ff02d..c538289e0 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -26,7 +26,7 @@ export const Example: Story = { const [editValue, setEditValue] = useState('edit me'); return ( - + {editValue} ); @@ -45,7 +45,7 @@ export const EditTitle: Story = { return (
- +

{editValue}

@@ -59,7 +59,7 @@ export const EditCopy: Story = { return (
- + {editValue} @@ -77,7 +77,7 @@ export const WithTextarea: Story = { } > {editValue} @@ -102,7 +102,7 @@ export const InForm: Story = { } > {editValue || 'Enter a value'} @@ -125,7 +125,7 @@ export const Controlled: Story = { onCancel={() => setEditing(false)} onEdit={() => setEditing(true)} {...args} - onSave={(value) => { + onConfirm={(value) => { setEditValue(value); setEditing(false); }} From e56990a971345aed79a9428229c01f128782c071 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 11:11:36 -0400 Subject: [PATCH 22/26] fix: update cy test --- packages/inline-edit/__tests__/InlineEdit.cy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/inline-edit/__tests__/InlineEdit.cy.tsx b/packages/inline-edit/__tests__/InlineEdit.cy.tsx index feda04465..023f49795 100644 --- a/packages/inline-edit/__tests__/InlineEdit.cy.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.cy.tsx @@ -4,7 +4,7 @@ describe('InlineEdit', () => { it('renders', () => { const editValue = 'test'; cy.mount( - undefined}> + undefined}> {editValue} ); @@ -14,7 +14,7 @@ describe('InlineEdit', () => { it('is accessible', () => { const editValue = 'test'; cy.mount( - undefined} aria-label="edit value"> + undefined} aria-label="edit value"> {editValue} ); From 53f36ba04a02c1192272bc7048b1ff9beca8a683 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 15:18:22 -0400 Subject: [PATCH 23/26] feat: cancel on blur --- .changeset/chatty-tables-attend.md | 1 + .../inline-edit/__tests__/InlineEdit.spec.tsx | 51 +++++++++++++++---- packages/inline-edit/package.json | 1 + packages/inline-edit/src/InlineEdit.tsx | 19 +++++-- .../stories/InlineEdit.stories.tsx | 9 ++-- pnpm-lock.yaml | 3 ++ 6 files changed, 67 insertions(+), 17 deletions(-) diff --git a/.changeset/chatty-tables-attend.md b/.changeset/chatty-tables-attend.md index a68c496f7..aacfece68 100644 --- a/.changeset/chatty-tables-attend.md +++ b/.changeset/chatty-tables-attend.md @@ -13,3 +13,4 @@ Add `inline-edit` package to display and allow inline editing of a form elements - Add handlers for edit, cancel, and confirm actions - Use prop `isEditing` to allow full control over the read and edit modes - Add `@vanilla-extract/css` as a peer dependency for prop `layout` variant types +- Use `useFocusWithin` to cancel edit on blur diff --git a/packages/inline-edit/__tests__/InlineEdit.spec.tsx b/packages/inline-edit/__tests__/InlineEdit.spec.tsx index 46cccce80..9c880f187 100644 --- a/packages/inline-edit/__tests__/InlineEdit.spec.tsx +++ b/packages/inline-edit/__tests__/InlineEdit.spec.tsx @@ -84,9 +84,8 @@ describe('InlineEdit', () => { const user = userEvent.setup(); render(); - await waitFor(() => { - screen.getByLabelText('edit').click(); - }); + await user.tab(); + await user.keyboard('{Enter}'); await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); @@ -102,9 +101,8 @@ describe('InlineEdit', () => { const user = userEvent.setup(); render(); - await waitFor(() => { - screen.getByLabelText('edit').click(); - }); + await user.tab(); + await user.keyboard('{Enter}'); await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); @@ -125,9 +123,8 @@ describe('InlineEdit', () => { const user = userEvent.setup(); render(); - await waitFor(() => { - screen.getByLabelText('edit').click(); - }); + await user.tab(); + await user.keyboard('{Enter}'); await waitFor(async () => { expect(screen.getByTestId('text-field')).toHaveFocus(); @@ -204,4 +201,40 @@ describe('InlineEdit', () => { expect(screen.getByLabelText('edit')).toBeVisible(); }); }); + + it('cancels when input is blurred', async () => { + const user = userEvent.setup(); + render(); + + await user.tab(); + await user.keyboard('{Enter}'); + + await waitFor(async () => { + expect(screen.getByTestId('text-field')).toHaveFocus(); + await user.tab({ shift: true }); + }); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + }); + + it('cancels when cancel button is blurred', async () => { + const user = userEvent.setup(); + render(); + + await user.tab(); + await user.keyboard('{Enter}'); + + await waitFor(async () => { + expect(screen.getByTestId('text-field')).toHaveFocus(); + await user.tab(); + await user.tab(); + await user.tab(); + }); + + await waitFor(() => { + expect(screen.getByLabelText('edit')).toBeVisible(); + }); + }); }); diff --git a/packages/inline-edit/package.json b/packages/inline-edit/package.json index a1ba73989..e90415dba 100644 --- a/packages/inline-edit/package.json +++ b/packages/inline-edit/package.json @@ -34,6 +34,7 @@ "dependencies": { "@react-aria/button": "3.8.0", "@react-aria/focus": "3.13.0", + "@react-aria/interactions": "3.16.0", "@react-aria/utils": "3.18.0", "@launchpad-ui/button": "workspace:~", "@launchpad-ui/form": "workspace:~", diff --git a/packages/inline-edit/src/InlineEdit.tsx b/packages/inline-edit/src/InlineEdit.tsx index 391732027..778bb0ac0 100644 --- a/packages/inline-edit/src/InlineEdit.tsx +++ b/packages/inline-edit/src/InlineEdit.tsx @@ -13,6 +13,7 @@ import { TextField } from '@launchpad-ui/form'; import { Icon } from '@launchpad-ui/icons'; import { useButton } from '@react-aria/button'; import { focusSafely } from '@react-aria/focus'; +import { useFocusWithin } from '@react-aria/interactions'; import { mergeProps, useUpdateEffect } from '@react-aria/utils'; import { cx } from 'classix'; import { cloneElement, useRef, useState } from 'react'; @@ -51,6 +52,7 @@ const InlineEdit = ({ confirmButtonLabel = 'confirm', }: InlineEditProps) => { const [isEditing, setEditing] = useState(isEditingProp ?? false); + const [isFocusWithin, setFocusWithin] = useState(false); const inputRef = useRef(null); const editRef = useRef(null); const controlled = isEditingProp !== undefined; @@ -62,9 +64,11 @@ const InlineEdit = ({ }, [isEditingProp]); useUpdateEffect(() => { - isEditing - ? inputRef.current && focusSafely(inputRef.current) - : editRef.current && focusSafely(editRef.current); + if (isFocusWithin) { + isEditing + ? inputRef.current && focusSafely(inputRef.current) + : editRef.current && focusSafely(editRef.current); + } }, [isEditing]); const handleEdit = () => { @@ -92,6 +96,11 @@ const InlineEdit = ({ } }; + const { focusWithinProps } = useFocusWithin({ + onBlurWithin: () => isEditing && handleCancel(), + onFocusWithinChange: (isFocusWithin) => setFocusWithin(isFocusWithin), + }); + const { buttonProps } = useButton( { 'aria-label': editButtonLabel, @@ -129,7 +138,7 @@ const InlineEdit = ({ ); return isEditing ? ( -
+
{input}
) : ( -
+
{renderReadContent}
); diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index c538289e0..8f56e3302 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -26,9 +26,12 @@ export const Example: Story = { const [editValue, setEditValue] = useState('edit me'); return ( - - {editValue} - + <> + + {editValue} + + + ); }, play: async ({ canvasElement }: { canvasElement: HTMLElement }) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb5ed5835..d4373cdf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -824,6 +824,9 @@ importers: '@react-aria/focus': specifier: 3.13.0 version: 3.13.0(react@18.2.0) + '@react-aria/interactions': + specifier: 3.16.0 + version: 3.16.0(react@18.2.0) '@react-aria/utils': specifier: 3.18.0 version: 3.18.0(react@18.2.0) From 721c835bb72a486af57a7344cedc7fba6a4feefc Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 15:40:19 -0400 Subject: [PATCH 24/26] fix: remove test button --- packages/inline-edit/stories/InlineEdit.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/inline-edit/stories/InlineEdit.stories.tsx b/packages/inline-edit/stories/InlineEdit.stories.tsx index 8f56e3302..2785c84b0 100644 --- a/packages/inline-edit/stories/InlineEdit.stories.tsx +++ b/packages/inline-edit/stories/InlineEdit.stories.tsx @@ -30,7 +30,6 @@ export const Example: Story = { {editValue} - ); }, From 5170e5fbb2f75327eb4dfb47f14c44efd8c9ceb3 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 16:16:14 -0400 Subject: [PATCH 25/26] chore: remove nyc config --- nyc.config.js | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 nyc.config.js diff --git a/nyc.config.js b/nyc.config.js deleted file mode 100644 index d7156b078..000000000 --- a/nyc.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - exclude: ['cypress/', 'packages/icons/src/!(Icon.tsx|StatusIcon.tsx|FlairIcon.tsx)'], -}; From 9aaa2c5a297bf25910623b685ed8c1b673b57156 Mon Sep 17 00:00:00 2001 From: Robb Niznik Date: Wed, 26 Jul 2023 16:37:02 -0400 Subject: [PATCH 26/26] fix: let cypress retry --- cypress.config.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index 43f3760d5..b604b3aee 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -19,9 +19,5 @@ export default defineConfig({ }, video: false, screenshotOnRunFailure: false, - env: { - codeCoverage: { - exclude: ['cypress/**/*.*', 'packages/icons/src/!(Icon.tsx|StatusIcon.tsx|FlairIcon.tsx)'], - }, - }, + retries: 1, });