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(web-components): add Dropdown, Listbox, and Option components #32116

Open
wants to merge 58 commits into
base: master
Choose a base branch
from

Conversation

radium-v
Copy link
Contributor

@radium-v radium-v commented Jul 25, 2024

New Behavior

Adds <fluent-dropdown>, <fluent-listbox> and <fluent-option> components.

Todo

  • Dropdown
    • ensure anchor positioning polyfill support
    • small size
    • large size
    • transparent appearance
    • filled-lighter appearance
    • filled-darker appearance
    • inline layout
    • disabled state
    • clearable mode
    • truncation
  • Option
    • start slot for icons
    • multi-line content
    • disabled state
  • Listbox
    • Empty state
  • Combobox
    • freeform option
    • filtering
    • multiselect with value string
  • Tests
  • Benchmarks
  • Stories

Related Issue(s)

@fabricteam
Copy link
Collaborator

fabricteam commented Jul 25, 2024

📊 Bundle size report

✅ No changes found

@KingOfTac
Copy link

@radium-v I noticed something interesting trying to use this in a dialog. The dropdown's beforetoggle handler never gets called for some reason. I'm not sure why this happens since the popover API supports nested popover elements

@@ -0,0 +1,7 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual regressions to review in the fluentuiv8 Visual Regression Report

Callout 5 screenshots
Image Name Diff(in Pixels) Image Type
Callout.Left top edge.chromium.png 1949 Changed
Callout.Beak 25.chromium.png 2186 Changed
Callout.Bottom left edge.chromium.png 2309 Changed
Callout.Right top edge.chromium.png 1127 Changed
Callout.No callout width specified.chromium.png 2319 Changed
react-charting-AreaChart 1 screenshots
Image Name Diff(in Pixels) Image Type
react-charting-AreaChart.Custom Accessibility.chromium.png 11 Changed

@fabricteam
Copy link
Collaborator

🕵 FluentUIV0 No visual regressions between this PR and main

@radium-v radium-v force-pushed the users/radium-v/wc-dropdown branch from 5c1d661 to cb1a6c9 Compare November 27, 2024 20:02
Copy link

github-actions bot commented Nov 27, 2024

📊 Bundle size report

✅ No changes found

Copy link

Pull request demo site: URL

packages/web-components/src/checkbox/checkbox.styles.ts Outdated Show resolved Hide resolved
packages/web-components/src/checkbox/checkbox.ts Outdated Show resolved Hide resolved
packages/web-components/src/checkbox/checkbox.ts Outdated Show resolved Hide resolved
packages/web-components/src/dropdown/dropdown.template.ts Outdated Show resolved Hide resolved
packages/web-components/src/dropdown/dropdown.styles.ts Outdated Show resolved Hide resolved
${ref('popoverContainer')}
>
<slot ${ref('listboxSlot')}></slot>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

With this DOM structure, you may be able to get away with not using the CSS Anchor Positioning polyfill at all, because the listbox/popover would be positioned correctly at its natural flow position, it won't flip or change size based on the trigger, but that might be an acceptable downgrade.

packages/web-components/src/listbox/listbox.styles.ts Outdated Show resolved Hide resolved
packages/web-components/src/option/option.styles.ts Outdated Show resolved Hide resolved
@radium-v radium-v force-pushed the users/radium-v/wc-dropdown branch from cb1a6c9 to e74eb0b Compare December 7, 2024 01:06
@radium-v radium-v force-pushed the users/radium-v/wc-dropdown branch 2 times, most recently from 134af2c to e79a83e Compare December 19, 2024 20:38
@radium-v radium-v changed the title feat(web-components): add Dropdown and Option components feat(web-components): add Dropdown, Combobox, Listbox, and Option components Dec 19, 2024
@radium-v radium-v marked this pull request as ready for review December 19, 2024 20:39
@radium-v radium-v requested a review from a team as a code owner December 19, 2024 20:39
@radium-v radium-v changed the title feat(web-components): add Dropdown, Combobox, Listbox, and Option components feat(web-components): add Dropdown, Listbox, and Option components Dec 19, 2024
@radium-v radium-v requested a review from a team as a code owner December 20, 2024 00:10
@radium-v radium-v requested a review from marchbox December 20, 2024 20:06
@radium-v radium-v force-pushed the users/radium-v/wc-dropdown branch from 99050ec to 3761ed8 Compare December 21, 2024 01:10
@@ -1,7 +1,8 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual regressions to review in the fluentui-web-components-v3 Visual Regression Report

Avatar 1 screenshots
Image Name Diff(in Pixels) Image Type
Avatar.Image.chromium.png 248 Changed

@@ -1,7 +1,8 @@
{
"extends": "../../tsconfig.base.wc.json",
"compilerOptions": {
"target": "ES2019",
"target": "ES2022",
"useDefineForClassFields": false,
Copy link
Contributor

Choose a reason for hiding this comment

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

can we move these to tsconfig.base.wc.json ?

Copy link
Contributor

Choose a reason for hiding this comment

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

with these added t obase we can remove it per project configs right ?

* An Option Custom HTML Element.
* Implements the {@link https://w3c.github.io/aria/#option | ARIA option } role.
*
* @slot checked-indicator - The checked indicator
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing the default slot, description slot, and the start slot.

* @slot checked-indicator - The checked indicator
* @slot indeterminate-indicator - The indeterminate indicator
* @fires change - Emits a custom change event when the checked state changes
* @fires input - Emits a custom input event when the checked state changes
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't seem these 2 events are being emitted.


/**
* A Dropdown Custom HTML Element.
* Implements the {@link https://w3c.github.io/aria/#combobox | ARIA combobox } role.
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing documentation for slots.

* @remarks
* HTML Attribute: `id`
*/
@attr({ attribute: 'id' })
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need mode: 'fromView'?

Copy link
Contributor

@Hotell Hotell left a comment

Choose a reason for hiding this comment

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

v-build owned files ✅

Copy link
Contributor

@davatron5000 davatron5000 left a comment

Choose a reason for hiding this comment

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

Couple legitimate change requests on the big boy Dropdown.ts, but mostly just notes to leave comments on why we're diverting from the norm on a couple occasions.

Overall great work @radium-v. Very clean and pretty code. It was pleasant to read through.

// Warning: (ae-forgotten-export) The symbol "Start" needs to be exported by the entry point index.d.ts
//
// @public
class Option_2 extends FASTElement implements Start {
Copy link
Contributor

Choose a reason for hiding this comment

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

Option_2 seems wrong here. Maybe we need to re-run API docs?

Comment on lines +20 to +50
export const dropdownInputTemplate = html<BaseDropdown>`
<input
@input="${(x, c) => x.inputHandler(c.event as InputEvent)}"
@change="${(x, c) => x.changeHandler(c.event as InputEvent)}"
aria-activedescendant="${x => x.activeDescendant}"
aria-controls="${x => x.listbox?.id ?? null}"
aria-labelledby="${x => x.ariaLabelledBy}"
aria-expanded="${x => x.open}"
aria-haspopup="listbox"
placeholder="${x => x.placeholder}"
role="combobox"
?disabled="${x => x.disabled}"
type="${x => x.type}"
value="${x => x.valueAttribute}"
${ref('control')}
/>
`;

export const dropdownButtonTemplate = html<BaseDropdown>`
<button
aria-activedescendant="${x => x.activeDescendant}"
aria-controls="${x => x.listbox?.id ?? null}"
aria-expanded="${x => x.open}"
aria-haspopup="listbox"
role="combobox"
?disabled="${x => x.disabled}"
${ref('control')}
>
${x => x.displayValue}
</button>
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Because these both had ref('control') and role="combobox" I was a little bit confused on how they're being used in the same template. I see later in BaseDropdown#insertControl that we conditionally insert these based on if it's a DropdownType.combobox. We could rename dropdownInputTemplate to comboboxInputTemplate to disambiguate but maybe the cleanest solution is to leave a comment for future us on why both of these exist.

Re: insertControl - this also seems like a good thing to document why we're using a slightly different approach to inject templates instead of something like a when. I see that method wipes out any slotted controls and injects either one of these templates, but it injects it into the root and not the slot?

I think I'm confused about how that's intended to work. If insertControl is auto-called by typeChanged and connectedCallback can a consumer supply their own dropdown templates without forking the base class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because these both had ref('control') and role="combobox" I was a little bit confused on how they're being used in the same template. I see later in BaseDropdown#insertControl that we conditionally insert these based on if it's a DropdownType.combobox. We could rename dropdownInputTemplate to comboboxInputTemplate to disambiguate but maybe the cleanest solution is to leave a comment for future us on why both of these exist.

Documentation comments incoming!

Re: insertControl - this also seems like a good thing to document why we're using a slightly different approach to inject templates instead of something like a when. I see that method wipes out any slotted controls and injects either one of these templates, but it injects it into the root and not the slot?

The input is inserted into the light dom, and assigned to the control slot. It's done in conjunction with slotAssignment: "manual" which is set in the element definition. This is 100% to sidestep cross-root ARIA limitations. Using when would place the input into the shadow dom, which would break ARIA. I'll make sure to add documentation so that this is clearly explained.

I think I'm confused about how that's intended to work. If insertControl is auto-called by typeChanged and connectedCallback can a consumer supply their own dropdown templates without forking the base class?

This is a great question. I'll investigate ways to possibly roll both of the templates into the main one, so that they're not pulled in directly.

Comment on lines +839 to +840
const rect = this.getBoundingClientRect();
if (rect.top < window.innerHeight - rect.bottom) {
Copy link
Contributor

Choose a reason for hiding this comment

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

getBoundingClientRect() and window.innerHeight both trigger a forced layout reflow which could have some perf problems inside a window.onscroll handler. At a minimum we'd want to debounce this. At a maximum, we'd want to figure out an alternative? like max-height: 40vh or something?

https://gist.github.com/paulirish/5d52fb081b3570c81e3a

Comment on lines +983 to +999
const observerCallback = (entries: IntersectionObserverEntry[]): void => {
entries.forEach(entry => {
const target = entry.target as Listbox;
if (isListbox(target)) {
if (inRange(entry.intersectionRatio, 0, 1)) {
toggleState(
target.dropdown?.elementInternals,
'flip-block',
entry.intersectionRect.bottom >= window.innerHeight,
);
}
}
});
};

Dropdown.AnchorPositionFallbackObserver =
Dropdown.AnchorPositionFallbackObserver ?? new IntersectionObserver(observerCallback, { threshold: [0, 1] });
Copy link
Contributor

Choose a reason for hiding this comment

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

Some comments on why we needed to setup this intersection observer would be nice.


Dropdown.AnchorPositionFallbackStyleElements.set(this, this.anchorPositionFallbackStyleElement);

Dropdown.AnchorPositionFallbackTimeout = requestIdleCallback(
Copy link
Contributor

Choose a reason for hiding this comment

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

Safari doesn't have requestIdleCallback yet 😭 only in Technical Preview as of now. Maybe just use requestAnimationFrame or setTimeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Component: Dropdown
8 participants