Fix/8944 file picker maintain state#9362
Conversation
… function readability
…8944-file-picker-maintain-state
|
👋 Since this is a community submitted pull request, a Buildkite build has not been started automatically. Would an Elastic organization member please verify the contents of this pull request and kick off a build manually? |
|
|
||
| return ( | ||
| <div> | ||
| <p style={{ marginBottom: 16 }}> |
There was a problem hiding this comment.
Let's use EUI components instead here to ensure baseline styles:
// p -> EuiText
<EuiText>
<p></p>
</EuiText>
// button -> EuiButton
<EuiButton>...</EuiButton>There was a problem hiding this comment.
A note on the custom styling: We generally don't use inline styles unless it's a dynamic style. If you want custom styles, use css to apply an Emotion style instead.
That being said, for these kind of spacing-only needs you can rather use EuiSpacer to space out your components.
Some general guidelines for choosing what to use:
EuiSpacer: when you need a simple spacing between elements, no need to apply an Emotion style for it (it's also valid though)- custom styles via
css:- whenever you have more styles than just spacing, you can combine them instead of adding a
EuiSpacer - whenever you need specific conditional spacing that you can't (or don't want to) handle with conditional rendering
- whenever you have more styles than just spacing, you can combine them instead of adding a
- custom styles via
styleattribute: for dynamic styles only
There was a problem hiding this comment.
Done.
Refactored the code as per your suggestions
| * This is useful for multi-step forms where the component may unmount | ||
| * and remount while the file data is stored in context/state. | ||
| */ | ||
| export const ControlledWithFilesProp: Story = { |
There was a problem hiding this comment.
Tiniest nit: Imho this can be shortened to ControlledWithFiles
| onClick={() => setShowPicker(!showPicker)} | ||
| style={{ marginBottom: 16 }} | ||
| > | ||
| Toggle Picker (Currently: {showPicker ? 'Visible' : 'Hidden'}) |
There was a problem hiding this comment.
Nit: Imho, we don't need to specify the visual state as part of the label, the render output is clear by itself (rendered, not rendered). Let's instead rename the label to be more precise on what it toggles, e.g. {showPicker ? 'Hide' : 'Show'} Picker.
| * and remount while the file data is stored in context/state. | ||
| */ | ||
| export const ControlledWithFilesProp: Story = { | ||
| render: function Render() { |
There was a problem hiding this comment.
Let's pass down the Storybook args to the EuiFilePicker. This allows the controls to be properly set up and for us to define some controls to be useable for the story.
Let's also add what controls we would want to be displayed/useable for testing and documentation:
export const ControlledWithFilesProp: Story = {
parameters: {
controls: {
include: ['files', 'multiple', 'disabled'], // adding multiple to provide means to test with multiple files
},
},
...
}|
|
||
| generatedId: string = htmlIdGenerator()(); | ||
|
|
||
| getPromptTextFromFileList = ( |
There was a problem hiding this comment.
We should be able to reuse this here in handleChange
And I think we can simplify it a bit with that:
if (this.fileInput.files && this.fileInput.files.length === 1) {
this.setState({ promptText: this.fileInput.value.split('\\').pop() });
} else {
this.setState({
promptText: this.getPromptTextFromFileList(
this.fileInput.files ? Array.from(this.fileInput.files) : null
),
});
}| isHoveringDrop: false, | ||
| }; | ||
|
|
||
| componentDidUpdate( |
There was a problem hiding this comment.
💭 I know the this wasn't specifically touched, but considering that we have componentDidUpdate now here, I think we should update removeFiles (code).
Currently it will do the following:
removeFiles -> this.handleChange() -> onChange(this.fileInput.files)
If I'm reading this correctly, then this means that the consumer that stores the files argument coming from onChange and passes it back to the component as files will have an empty array. And if the consumer has a condition check for updating state based on onChange that looks something like e.g. files && files.length > 0) then the files are not updated. Passing null will ensure it's fully unset.
This could be changed here:
onChange(
this.fileInput.files && this.fileInput.files.length > 0
? this.fileInput.files
: null
);| expect(screen.getByText('updated-file.txt')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| test('shows clear button when files prop is set', () => { |
There was a problem hiding this comment.
We should also test that:
- clears file name on clear
- clicking the clear button triggers the
onChange - clear button is removed after clearing
packages/eui/src/components/form/file_picker/file_picker.test.tsx
Outdated
Show resolved
Hide resolved
| * the displayed state (file names in the prompt). The actual file data | ||
| * should be stored and managed separately in your application state. | ||
| */ | ||
| files?: FileList | null; |
There was a problem hiding this comment.
While this is correct to align it with the native type passed in onChange, I would suggest to use File[] instead. Since we can't pass the files back to the <input> element anyway, having an array passed could make the usage for consumers easier as it is easier to construct and handle it on consumer side.
It does require the consumer to use e.g. Array.from() on the onChange argument but but imho that should be acceptable for a more flexible API.
There was a problem hiding this comment.
Done.
Using files?: File[] | null; now
|
💭 General thought: |
+1. |
Summary
This PR adds a new
filesprop toEuiFilePickerthat allows developers to control the displayed file state between renders.The implementation:
filesprop of typeFileList | nullpromptTextstate from thefilesprop if providedcomponentDidUpdateto sync the displayed state when thefilesprop changesgetPromptTextFromFileListmethodNote: Due to browser security restrictions, the actual
<input type="file">cannot be programmatically set with files. This prop only controls the displayed state (file names in the prompt). The actual file data should be stored and managed separately in application state.Testing: Can be tested locally at http://localhost:6006/?path=/story/forms-euifilepicker--controlled-with-files-prop
Why are we making this change?
Closes #8944
When using
EuiFilePickerin multi-page forms or wizards, the file picker resets its visual state when unmounted, even though the file data may still be stored in application context. This causes user confusion as they navigate back to a step and see an empty file picker despite having already selected files.This change allows developers to pass a
filesprop to maintain the displayed state, providing a better user experience for multi-step workflows.Screenshots #
N/A - No visual changes. The component displays the same file information, but now can be controlled via props.
Screen.Recording.2026-02-24.at.9.33.55.PM.mov
Impact to users
Non-breaking change. This is an additive feature that introduces a new optional
filesprop. Existing implementations will continue to work unchanged.Users who want to maintain file picker state across re-renders can now pass stored
FileListdata to thefilesprop to display the previously selected file names.QA
General checklist
Added documentation@defaultif default values are missing) and playground togglesChecked Code Sandbox works for any docs examplesUpdated visual regression tests(No visual changes)If applicable, added the breaking change issue label(Not a breaking change)If the changes unblock an issue in a different repo, smoke tested carefullyIf applicable, file an issue to update EUI's Figma library(No visual/design changes)