ui icon indicating copy to clipboard operation
ui copied to clipboard

feat(button): add loadingPosition prop for loading icon placement control

Open yeonjulee1005 opened this issue 2 months ago โ€ข 2 comments

๐Ÿ”— Linked issue

#5362

โ“ Type of change

  • [ ] ๐Ÿ“– Documentation (updates to the documentation or readme)
  • [ ] ๐Ÿž Bug fix (a non-breaking change that fixes an issue)
  • [ ] ๐Ÿ‘Œ Enhancement (improving an existing functionality)
  • [x] โœจ New feature (a non-breaking change that adds functionality)
  • [ ] ๐Ÿงน Chore (updates to the build process or auxiliary tools and libraries)
  • [ ] โš ๏ธ Breaking change (fix or feature that would cause existing functionality to change)

๐Ÿ“š Description

Adds a new loadingPosition prop to the Button component that allows developers to control where the loading icon appears when the button is in a loading state.

Features:

  • Three position options: left, center, and right
  • left: displays loading icon on the left (default behavior)
  • center: displays loading icon in the center, hiding the label
  • right: displays loading icon on the right

https://github.com/user-attachments/assets/818c22ed-09dd-4a54-87ff-085fc5ce4b09

Backward Compatibility:

  • Maintains full backward compatibility with existing code
  • Existing usage with leading and trailing props continues to work without any changes
  • When loadingPosition is not specified, the component automatically determines the position based on the trailing prop

Implementation Details:

  • Added loadingPosition prop to ButtonProps interface
  • Updated useComponentIcons logic to handle position-based loading icon placement
  • Added conditional rendering for center position (hides label, shows only loading icon)
  • Added comprehensive test cases for all three position options

Documentation:

  • Added new "Loading Position" section in button.md
  • Includes code examples for each position option
  • Added backward compatibility note

๐Ÿ“ Checklist

  • [x] I have linked an issue or discussion.
  • [x] I have updated the documentation accordingly.

yeonjulee1005 avatar Nov 15 '25 01:11 yeonjulee1005

npm i https://pkg.pr.new/@nuxt/ui@5456

commit: db780e9

pkg-pr-new[bot] avatar Nov 15 '25 01:11 pkg-pr-new[bot]

Thank you! :)

yeonjulee1005 avatar Nov 15 '25 01:11 yeonjulee1005

First off -- thank you. I have some comments and questions.

  1. I'm not sure whether this needs to be a UButton-specific feature, or if it could be useful for other components and should instead be implemented in useComponentIcons -- when I was making the issue I have not read icon positioning source code, but now that I have I see that the icon positioning logic is reused between Badge, Input*, Select* and Textarea, and it may be useful there as well.

  2. In the documentation you say:

    Use the loading-position prop to control where the loading icon appears. The prop accepts three values:

    • left: displays loading icon on the left (default)

    In but in the issue you say:

    Backward Compatibility:

    • When loadingPosition is not specified, the component automatically determines the position based on the trailing prop

    Which one is it? Current behaviour is not putting the loading spinner at the start of the button, but instead follows a complex logic that I was not able to simplify much yet:

    ScreenshotScreenshot 2025-11-15 at 12-55-15
    Source
    <script setup lang="ts">
    const args: {
      icon?: string;
      leading?: boolean;
      trailing?: boolean;
      leadingIcon?: string;
      trailingIcon?: string;
    }[] = [
      {},
      { trailingIcon: "i-lucide-chevron-right" },
      { trailing: true },
      { trailing: true, trailingIcon: "i-lucide-chevron-right" },
      { leadingIcon: "i-lucide-chevron-left" },
      {
        leadingIcon: "i-lucide-chevron-left",
        trailingIcon: "i-lucide-chevron-right",
      },
      { leadingIcon: "i-lucide-chevron-left", trailing: true },
      {
        leadingIcon: "i-lucide-chevron-left",
        trailing: true,
        trailingIcon: "i-lucide-chevron-right",
      },
      { leading: true },
      { leading: true, trailingIcon: "i-lucide-chevron-right" },
      { leading: true, trailing: true },
      { leading: true, trailing: true, trailingIcon: "i-lucide-chevron-right" },
      { leading: true, leadingIcon: "i-lucide-chevron-left" },
      {
        leading: true,
        leadingIcon: "i-lucide-chevron-left",
        trailingIcon: "i-lucide-chevron-right",
      },
      { leading: true, leadingIcon: "i-lucide-chevron-left", trailing: true },
      {
        leading: true,
        leadingIcon: "i-lucide-chevron-left",
        trailing: true,
        trailingIcon: "i-lucide-chevron-right",
      },
      { icon: "i-lucide-chevrons-left-right" },
      {
        icon: "i-lucide-chevrons-left-right",
        trailingIcon: "i-lucide-chevron-right",
      },
      { icon: "i-lucide-chevrons-left-right", trailing: true },
      {
        icon: "i-lucide-chevrons-left-right",
        trailing: true,
        trailingIcon: "i-lucide-chevron-right",
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leadingIcon: "i-lucide-chevron-left",
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leadingIcon: "i-lucide-chevron-left",
        trailingIcon: "i-lucide-chevron-right",
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leadingIcon: "i-lucide-chevron-left",
        trailing: true,
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leadingIcon: "i-lucide-chevron-left",
        trailing: true,
        trailingIcon: "i-lucide-chevron-right",
      },
      { icon: "i-lucide-chevrons-left-right", leading: true },
      {
        icon: "i-lucide-chevrons-left-right",
        leading: true,
        trailingIcon: "i-lucide-chevron-right",
      },
      { icon: "i-lucide-chevrons-left-right", leading: true, trailing: true },
      {
        icon: "i-lucide-chevrons-left-right",
        leading: true,
        trailing: true,
        trailingIcon: "i-lucide-chevron-right",
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leading: true,
        leadingIcon: "i-lucide-chevron-left",
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leading: true,
        leadingIcon: "i-lucide-chevron-left",
        trailingIcon: "i-lucide-chevron-right",
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leading: true,
        leadingIcon: "i-lucide-chevron-left",
        trailing: true,
      },
      {
        icon: "i-lucide-chevrons-left-right",
        leading: true,
        leadingIcon: "i-lucide-chevron-left",
        trailing: true,
        trailingIcon: "i-lucide-chevron-right",
      },
    ];
    </script>
    
    <template>
      <div class="flex flex-col gap-2 p-4">
        <div v-for="(arg, key) of args" :key class="flex gap-2">
          <UButton
            label="Label"
            :icon="arg.icon"
            :leading="arg.leading"
            :trailing="arg.trailing"
            :leading-icon="arg.leadingIcon"
            :trailing-icon="arg.trailingIcon"
          />
          <UButton
            label="Label"
            loading
            :icon="arg.icon"
            :leading="arg.leading"
            :trailing="arg.trailing"
            :leading-icon="arg.leadingIcon"
            :trailing-icon="arg.trailingIcon"
          />
          <code>{{ arg }}</code>
        </div>
      </div>
    </template>
    
  3. No sure whether left/right is better than leading/trailing -- firstly because it introduces new language when leading/trailing is already there, secondly because of potential issues with RTL locales, but not sure about that one.

rijenkii avatar Nov 15 '25 06:11 rijenkii

@rijenkii

Questions are always welcome!๐Ÿค—

  1. Thatโ€™s right! There are other components that also receive loading prop. However, since these components donโ€™t handle events like buttons do, I believe the provided value should always be displayed by default.

  2. I wanted to make sure existing users donโ€™t get confused. So while keeping the current logic where leading and trailing are chosen when using loading, I thought adding loadingPosition should still allow users to specify the intended placement.

  3. Other components also use left and right as prop values, so I felt that using those(include center also) would be clearer and more consistent than using leading or trailing.

yeonjulee1005 avatar Nov 15 '25 08:11 yeonjulee1005

I just looked at the implementation, and loading-position="center" does not do what I described in https://github.com/nuxt/ui/issues/5362: it does not keep the width of the button intact when toggling the loading.

The implemented behaviour (label disappears completely and is replaced by the loading icon, changing the width of the button):

https://github.com/user-attachments/assets/d75581a3-2c64-4698-9fc4-2511664e323f

The behaviour described in the issue (label and other icons receive opacity: 0 with the spinner rendered above them, keeping the width of the button):

https://github.com/user-attachments/assets/e21e8eeb-ffd9-4260-9737-d166d9890eb3

rijenkii avatar Nov 15 '25 08:11 rijenkii

@rijenkii Sure~! Iโ€™ll update it so that the changes are applied.

https://github.com/user-attachments/assets/21494528-e4a5-41cf-99dc-5bb47feace01

yeonjulee1005 avatar Nov 15 '25 11:11 yeonjulee1005