Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add inline-edit package #938

Merged
merged 26 commits into from
Jul 26, 2023
Merged

feat: add inline-edit package #938

merged 26 commits into from
Jul 26, 2023

Conversation

Niznikr
Copy link
Contributor

@Niznikr Niznikr commented Jul 24, 2023

Summary

Add inline-edit package to display and allow inline editing of a form elements:

  • 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
  • Use useFocusWithin to cancel edit on blur

Testing approaches

https://626696a2018c1f004a1cde86-biuafqdqpl.chromatic.com/

@changeset-bot
Copy link

changeset-bot bot commented Jul 24, 2023

🦋 Changeset detected

Latest commit: 9aaa2c5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@launchpad-ui/inline-edit Minor
@launchpad-ui/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

},
});

type InlineVariants = RecipeVariants<typeof inline>;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This Vanilla Extract utility returns type layout: "vertical" | "horizontal" for the prop which means we can have a single source of truth for props tied directly to component styles (variants).

@github-actions
Copy link
Contributor

github-actions bot commented Jul 24, 2023

Size Change: +38 B (0%)

Total Size: 161 kB

Filename Size Change
packages/core/dist/index.es.js 1.1 kB +16 B (+1%)
packages/core/dist/index.js 1.47 kB +22 B (+2%)
ℹ️ View Unchanged
Filename Size
packages/alert/dist/index.es.js 1.02 kB
packages/alert/dist/index.js 1.09 kB
packages/alert/dist/style.css 1.03 kB
packages/avatar/dist/index.es.js 1.16 kB
packages/avatar/dist/index.js 1.23 kB
packages/avatar/dist/style.css 466 B
packages/banner/dist/index.es.js 641 B
packages/banner/dist/index.js 711 B
packages/banner/dist/style.css 545 B
packages/button/dist/index.es.js 1.62 kB
packages/button/dist/index.js 1.7 kB
packages/button/dist/style.css 3.08 kB
packages/chip/dist/index.es.js 676 B
packages/chip/dist/index.js 746 B
packages/chip/dist/style.css 895 B
packages/clipboard/dist/index.es.js 1.51 kB
packages/clipboard/dist/index.js 1.59 kB
packages/clipboard/dist/style.css 829 B
packages/collapsible/dist/index.es.js 853 B
packages/collapsible/dist/index.js 919 B
packages/collapsible/dist/style.css 94 B
packages/columns/dist/index.es.js 619 B
packages/columns/dist/index.js 692 B
packages/columns/dist/style.css 354 B
packages/counter/dist/index.es.js 333 B
packages/counter/dist/index.js 396 B
packages/counter/dist/style.css 256 B
packages/drawer/dist/index.es.js 1.73 kB
packages/drawer/dist/index.js 2.29 kB
packages/drawer/dist/style.css 570 B
packages/dropdown/dist/index.es.js 1.14 kB
packages/dropdown/dist/index.js 1.2 kB
packages/filter/dist/index.es.js 2.3 kB
packages/filter/dist/index.js 2.37 kB
packages/filter/dist/style.css 1 kB
packages/focus-trap/dist/index.es.js 268 B
packages/focus-trap/dist/index.js 331 B
packages/form/dist/index.es.js 3.99 kB
packages/form/dist/index.js 4.1 kB
packages/form/dist/style.css 2.68 kB
packages/icons/dist/index.es.js 1.28 kB
packages/icons/dist/index.js 1.35 kB
packages/icons/dist/style.css 615 B
packages/inline-edit/dist/index.es.js 1.39 kB
packages/inline-edit/dist/index.js 1.49 kB
packages/inline-edit/dist/style.css 338 B
packages/inline/dist/index.es.js 565 B
packages/inline/dist/index.js 637 B
packages/inline/dist/style.css 299 B
packages/markdown/dist/index.es.js 960 B
packages/markdown/dist/index.js 1.03 kB
packages/markdown/dist/style.css 234 B
packages/menu/dist/index.es.js 3.55 kB
packages/menu/dist/index.js 3.63 kB
packages/menu/dist/style.css 1.22 kB
packages/modal/dist/index.es.js 3 kB
packages/modal/dist/index.js 3.56 kB
packages/modal/dist/style.css 1.03 kB
packages/navigation/dist/index.es.js 2.79 kB
packages/navigation/dist/index.js 2.85 kB
packages/navigation/dist/style.css 1.25 kB
packages/overlay/dist/index.es.js 1 kB
packages/overlay/dist/index.js 1.06 kB
packages/pagination/dist/index.es.js 1.18 kB
packages/pagination/dist/index.js 1.26 kB
packages/pagination/dist/style.css 356 B
packages/popover/dist/index.es.js 3.07 kB
packages/popover/dist/index.js 3.57 kB
packages/popover/dist/style.css 629 B
packages/portal/dist/index.es.js 390 B
packages/portal/dist/index.js 449 B
packages/progress-bubbles/dist/index.es.js 1.76 kB
packages/progress-bubbles/dist/index.js 1.83 kB
packages/progress-bubbles/dist/style.css 961 B
packages/progress/dist/index.es.js 1.02 kB
packages/progress/dist/index.js 1.09 kB
packages/progress/dist/style.css 278 B
packages/select/dist/index.es.js 5.91 kB
packages/select/dist/index.js 5.99 kB
packages/select/dist/style.css 1.34 kB
packages/slider/dist/index.es.js 579 B
packages/slider/dist/index.js 644 B
packages/slider/dist/style.css 672 B
packages/snackbar/dist/index.es.js 1.17 kB
packages/snackbar/dist/index.js 1.73 kB
packages/snackbar/dist/style.css 580 B
packages/split-button/dist/index.es.js 883 B
packages/split-button/dist/index.js 954 B
packages/split-button/dist/style.css 495 B
packages/stack/dist/index.es.js 494 B
packages/stack/dist/index.js 565 B
packages/stack/dist/style.css 226 B
packages/tab-list/dist/index.es.js 737 B
packages/tab-list/dist/index.js 809 B
packages/tab-list/dist/style.css 455 B
packages/table/dist/index.es.js 1.02 kB
packages/table/dist/index.js 1.1 kB
packages/table/dist/style.css 905 B
packages/tag/dist/index.es.js 2.85 kB
packages/tag/dist/index.js 2.92 kB
packages/tag/dist/style.css 945 B
packages/toast/dist/index.es.js 979 B
packages/toast/dist/index.js 1.53 kB
packages/toast/dist/style.css 544 B
packages/toggle/dist/index.es.js 764 B
packages/toggle/dist/index.js 844 B
packages/toggle/dist/style.css 1.52 kB
packages/tokens/dist/index.css 1.48 kB
packages/tokens/dist/index.es.js 1.93 kB
packages/tokens/dist/index.js 6.96 kB
packages/tokens/dist/media-queries.css 112 B
packages/tokens/dist/themes.css 1.11 kB
packages/tooltip/dist/index.es.js 514 B
packages/tooltip/dist/index.js 588 B
packages/tooltip/dist/style.css 366 B
packages/vars/dist/index.es.js 1.51 kB
packages/vars/dist/index.js 1.58 kB

compressed-size-action

Copy link
Contributor

@apucacao apucacao left a comment

Choose a reason for hiding this comment

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

Working well so far!


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
Copy link
Contributor

Choose a reason for hiding this comment

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

This means this component cannot be "controlled", right?

Are there use cases where we might want it to be controlled too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah having controlled can be useful! Will add it.

- 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Would naming this renderInput make this self-explanatory?

input = <TextField />,
'aria-label': ariaLabel,
}: InlineEditProps) => {
const [isEditing, setEditing] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question the one about defaultValue and un/controlled.

Are there use cases where it would be useful to pass isEditing from the owner component?

@Niznikr Niznikr marked this pull request as ready for review July 26, 2023 15:12
@Niznikr Niznikr requested review from a team, stinachen and lwonsower July 26, 2023 15:12
@Niznikr Niznikr requested a review from hisuida July 26, 2023 15:12
setEditing(false);
}}
>
<span>{editValue}</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you get a chance to try using a render function for children? The idea was that in uncontrolled mode, it would remove the need for consumers to manage the state of the field's value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did but was struggling with the DX. Do you have an idea what something like that would look like? Seems like folks would still need to use onConfirm to have access to the updated value to do something with it form/data wise.

!controlled && setEditing(false);
};

const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Was exploring the new stories in this branch locally, noticed that pressing Esc will cancel an in-progress edit which brought me here, wondering if we should also consider the case where a user blurs away from the input/cancel/save buttons while editing (either by clicking outside of the affected elements, or via keyboard navigation)?

There is some nuance to consider, but I'm wondering about the case where multiple inline-editable fields exist next to each other... how would a user navigate between them? For example: if I begin editing a field but then try to keyboard navigate to another editable field, what should happen? I'd expect the edit for the first field to be implicitly cancelled, but curious to hear your thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah no strong opinion on blur implicitly cancelling. Can try that out and see how that feels.

Copy link
Contributor

Choose a reason for hiding this comment

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

What are the downsides of leaving the inline edits in editing mode when tabbing around?

The escape has less ambiguous intent than tabbing around.

One way to think about this maybe: do we want this component to be modal, where interacting with another component would affect the state of this component? (Think popover: once it's open, if the user interacts with anything else on the page, it should close.)

Copy link
Contributor

@kwatkins-ld kwatkins-ld Jul 26, 2023

Choose a reason for hiding this comment

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

One question is predictability: by making the interaction modal we can create the affordance that the edit is only persisted if the user explicitly chooses to do so (by clicking the save button, or pressing Enter)... anything else causes the draft to be discarded. This question also has implications for when we might consider a form "dirty" (which overlaps with routing behavior).

In the absence of a modal interaction, it's hard to reason about why we'd choose inline edit at all... if a field that I begin editing can be left in the editing state to move on to another field, what value is inline-edit adding over plain inputs?

Also to be clear I think this is very much a question that requires design feedback, I had just started to think about this while implementing elsewhere so the questions are fresh in mind.

Copy link
Contributor

Choose a reason for hiding this comment

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

what value is inline-edit adding over plain inputs?

Yeah. There are places where we use an inline edit to make a field opt-in, like flag keys upon creation.

This question also has implications for when we might consider a form "dirty" (which overlaps with routing behavior).

How does this overlap with routing?

If the user hasn't confirmed their input, I'd expect form's state to be unchanged.

Anyways, these are good points for UX crit. I don't think these are that complicated, so long as we're consistent.

Copy link
Contributor

Choose a reason for hiding this comment

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

How does this overlap with routing?

Today we consider a form dirty if the input hasn't been saved, and dirty forms (in most places) cause route changes to be blocked and the user is forced to confirm the navigation... if we make this interaction modal, blurring a field that is being edited would discard the draft (and therefore not cause the field to be considered dirty).

Conversely, if this interaction isn't modal the following scenario seems possible: begin editing a field, change the field's value, then navigate to a different field... in that case the first field would need to be considered dirty to prevent unexpectedly losing edits.

I also agree that these questions aren't especially complicated, and that we don't necessarily have to answer them in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

That said, we should be opinionated here before we show up at UX crit.

My 2¢: Let's cancel on blur for now. That feels snappier to me. 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Meeting with Antonio shortly and can go over this topic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed with Antonio and we are gonna go with cancel on blur 👍

@apucacao
Copy link
Contributor

Any thoughts in making the entire component interactive when in non-editable mode? Clicking could enable editable mode. That would give us a larger click area, and bring us closer to the native contenteditable behavior. We could also give some subtle treatment to that interaction to make that clear.

Copy link
Contributor

@apucacao apucacao left a comment

Choose a reason for hiding this comment

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

This looks good to me.

There's lot of potential tweaks we can make to the interaction, but we can do that once this is already in people's hands.

Copy link
Contributor

@lwonsower lwonsower left a comment

Choose a reason for hiding this comment

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

This is awesome. 🙏🏼

@Niznikr Niznikr merged commit db9e620 into main Jul 26, 2023
10 checks passed
@Niznikr Niznikr deleted the feat/inline-edit branch July 26, 2023 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants