Skip to content

Fix/8944 file picker maintain state#9362

Open
ragini-pandey wants to merge 8 commits intoelastic:mainfrom
ragini-pandey:fix/8944-file-picker-maintain-state
Open

Fix/8944 file picker maintain state#9362
ragini-pandey wants to merge 8 commits intoelastic:mainfrom
ragini-pandey:fix/8944-file-picker-maintain-state

Conversation

@ragini-pandey
Copy link
Contributor

@ragini-pandey ragini-pandey commented Feb 6, 2026

Summary

This PR adds a new files prop to EuiFilePicker that allows developers to control the displayed file state between renders.

The implementation:

  • Added a new optional files prop of type FileList | null
  • Initializes promptText state from the files prop if provided
  • Added componentDidUpdate to sync the displayed state when the files prop changes
  • Extracts the file name display logic into a reusable getPromptTextFromFileList method

Note: 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 EuiFilePicker in 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 files prop 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 files prop. Existing implementations will continue to work unchanged.

Users who want to maintain file picker state across re-renders can now pass stored FileList data to the files prop to display the previously selected file names.

QA

General checklist

  • Browser QA
    • Checked in both light and dark modes
    • Checked in both MacOS and Windows high contrast modes
    • Checked in mobile
    • Checked in Chrome, Safari, Edge, and Firefox
    • Checked for accessibility including keyboard-only and screenreader modes
  • Docs site QA
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately
    • If applicable, added the breaking change issue label (Not a breaking change)
    • If the changes unblock an issue in a different repo, smoke tested carefully
  • Designer checklist
    • If applicable, file an issue to update EUI's Figma library (No visual/design changes)

@ragini-pandey ragini-pandey requested a review from a team as a code owner February 6, 2026 17:20
@github-actions
Copy link

github-actions bot commented Feb 6, 2026

👋 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?

@github-actions github-actions bot added the community contribution (Don't delete - used for automation) label Feb 6, 2026
@mgadewoll mgadewoll self-requested a review February 12, 2026 13:52

return (
<div>
<p style={{ marginBottom: 16 }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use EUI components instead here to ensure baseline styles:

// p -> EuiText
<EuiText>
  <p></p>
</EuiText>

// button -> EuiButton
<EuiButton>...</EuiButton>

Copy link
Contributor

Choose a reason for hiding this comment

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

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
  • custom styles via style attribute: for dynamic styles only

Copy link
Contributor Author

@ragini-pandey ragini-pandey Feb 24, 2026

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

Tiniest nit: Imho this can be shortened to ControlledWithFiles

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

onClick={() => setShowPicker(!showPicker)}
style={{ marginBottom: 16 }}
>
Toggle Picker (Currently: {showPicker ? 'Visible' : 'Hidden'})
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* and remount while the file data is stored in context/state.
*/
export const ControlledWithFilesProp: Story = {
render: function Render() {
Copy link
Contributor

Choose a reason for hiding this comment

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

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
    },
  },
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


generatedId: string = htmlIdGenerator()();

getPromptTextFromFileList = (
Copy link
Contributor

Choose a reason for hiding this comment

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

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
    ),
  });
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

isHoveringDrop: false,
};

componentDidUpdate(
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 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
);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

expect(screen.getByText('updated-file.txt')).toBeInTheDocument();
});

test('shows clear button when files prop is set', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also test that:

  • clears file name on clear
  • clicking the clear button triggers the onChange
  • clear button is removed after clearing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

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

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.
Using files?: File[] | null; now

@mgadewoll
Copy link
Contributor

💭 General thought: EuiFilePicker is still a class component. It could be an additional task (in a separate PR) to migrate it to a functional component.

@ragini-pandey
Copy link
Contributor Author

💭 General thought: EuiFilePicker is still a class component. It could be an additional task (in a separate PR) to migrate it to a functional component.

+1.
I am happy to work on it if anyone from EUI team creates an issue and help wanted label

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community contribution (Don't delete - used for automation)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[EuiFilePicker] Maintain state between renders

2 participants