goetzrobin / spartan

Cutting-edge tools powering Angular full-stack development.
https://spartan.ng
MIT License
1.44k stars 154 forks source link

RFC: Select #65

Closed thatsamsonkid closed 8 months ago

thatsamsonkid commented 11 months ago

Which scope/s are relevant/related to the feature request?

select

Information

Select

Deps

  1. CDKList
  2. CDKOverlay

brn

API (Proposed)

<brn-select formControlName="fruit" placeholder="Select a Fruit">
  <brn-select-trigger>
     <brn-select-value />
  </brn-select-trigger>
  <brn-select-content class="w-56">
      <brn-option value="Refresh">Refresh</fue-option>
      <brn-option value="Settings">Settings</fue-option>
      <brn-option value="Help">Help</fue-option>
      <brn-option value="Signout">Sign out</fue-option>
   </brn-select-content>
</brn-select>

1. brn-select - (Component)

Will be the primary parent component and will also be the point of contact for interfacing with the the underlying ngControl and other "input/form control" attributes that may need to be passed to the component. Radix and Shadcn have placeholder set on the select.value component but I think it's cleaner and more predictable to pass these things to the parent but I dont know would definitely like to hear from others.

Proposed Inputs

Input Type Default Description
placeholder string "" placeholder text
multiple boolean false Enables multi option selection
disable boolean false Enables or disables form control. Probably only really useful for ngModel since in reactive we will set the control as disabled

We can add anything else. I know looking at the mat-select they also include some eventEmitters we can also implement some of those if we think they would be useful. Maybe the emitting the open and close of select overlay would be useful as well.

2. brn-select-trigger - (Component)

Holds the button and will trigger open and closing the cdkOverlay.

Note: Reason why I believe we may need to use cdkOverlay over menu is I found cdkMenu to be a little limited especially when it came to allowing multi-selection. Maybe this something that can be changed I didnt find a way to keep it open after making a selection. I think mainly an issue with trying to use cdkMenuItem and cdkListboxOption at same time.

4. brn-select-value - (Component)

This component will simply display the current value for the select component. If no value we will display a provided placeholder text. User can also optionally exclude brn-select-value and instead pass there only template or html and can render the value using the formcontrol's value with either ngModel or reactive form group

5. brn-select-content (Directive)

Has cdkListbox as a host directive. Will listen for Listbox value changes and publish the value updates. Will also be responsible for setting additional attributes tot he cdkListbox such as a multiple and setting the focus on the cdkListbox when the overlay is opened. As well as setting aria label id's controlledBy and LabelledBy

6. brn-select-option (component)

CdkOption as a host directive, so this is a component primarily for holding the svg checkbox to show hide when an option is selected (Reason for component instead of directive). Not sure if you would want this to be more flexible in terms of being able to provide something custom

Input Type Default Description
value string "" Really just a passthrough for the value input for cdkOption directive

7. brn-select-separator (directive)

Unsure of this one, this may just be a helm since I dont believe there is any physical element or divider rendered just spacing from what I can tell

8. brn-select-scroll-up (directive)

Can probably just have a a mouseenter hostbinding to trigger and change the option focus on cdkList

9. brn-select-scroll-down (directive)

same as number 7

10. brn-select-service (Service)

With most of these component being passed as content children, much easier to manage all the state in a service. We can use a signal object like shown below.

state = signal<{
  id: string;
  labelId: string;
  panelId: string;
  placeholder: string;
  isExpanded: boolean;
  multiple: boolean;
  disabled: boolean;
  value: string | string[];
}>({
  id: "",
  labelId: "",
  panelId: "",
  placeholder: "",
  isExpanded: false,
  multiple: false,
  disabled: false,
  value: "",
});

We can have accessor's like this. Took inspiration for this from something I saw Joshua Morony do recently when using signals and some light state management. We can do this differently if needed just thought it would be interesting to try.

Reference: https://youtu.be/ol671CJnNjY?si=o5hYEJ_8dRzrvWJS

id = computed(() => this.state().id);
labelId = computed(() => this.state().labelId);
panelId = computed(() => this.state().panelId);
placeholder = computed(() => this.state().placeholder);
disabled = computed(() => this.state().disabled);
isExpanded = computed(() => this.state().isExpanded);
multiple = computed(() => this.state().multiple);
value = computed(() => this.state().value);

We also need a subject to emit changes from cdkListbox to the service and update the state in the service. All other components will just read the value from state signal.

listBoxValueChangeEvent$ = new Subject<ListboxValueChangeEvent<any>>();

hlm

We can have one for each brn component/directive to add associated styles with an addition to these

1. hlm-select-group (directive)

2. hlm-select-label (directive)

Aria

  1. Label - User can pass a label within select parent and we can manually assign an id if one is not provided to the label element. If no label element then select component can generate one an invisible one based on placeholder. Can correct me but i believe its usually best practice to have some sort of label

  2. we can generate id's for the select trigger and select content elements and provide controls and controlledBy + LabelledBy respectively.

  3. Aria expanded on select-trigger and role of combobox

  4. cdkList is taking care of all the Listbox ADA and keyboard navigation so no need to worry much about that.

Radix

<Select.Root>
  <Select.Trigger className="SelectTrigger" aria-label="Food">
    <Select.Value placeholder="Select a fruit…" />
    <Select.Icon className="SelectIcon">
      <ChevronDownIcon />
    </Select.Icon>
  </Select.Trigger>
  <Select.Portal>
    <Select.Content className="SelectContent">
      <Select.ScrollUpButton className="SelectScrollButton">
        <ChevronUpIcon />
      </Select.ScrollUpButton>
      <Select.Viewport className="SelectViewport">
        <Select.Group>
          <Select.Label className="SelectLabel">Fruits</Select.Label>
          <SelectItem value="apple">Apple</SelectItem>
          <SelectItem value="banana">Banana</SelectItem>
          <SelectItem value="blueberry">Blueberry</SelectItem>
          <SelectItem value="grapes">Grapes</SelectItem>
          <SelectItem value="pineapple">Pineapple</SelectItem>
        </Select.Group>

        <Select.Separator className="SelectSeparator" />

        <Select.Group>
          <Select.Label className="SelectLabel">Vegetables</Select.Label>
          <SelectItem value="aubergine">Aubergine</SelectItem>
          <SelectItem value="broccoli">Broccoli</SelectItem>
          <SelectItem value="carrot" disabled>
            {" "}
            Carrot{" "}
          </SelectItem>
          <SelectItem value="courgette">Courgette</SelectItem>
          <SelectItem value="leek">Leek</SelectItem>
        </Select.Group>

        <Select.Separator className="SelectSeparator" />

        <Select.Group>
          <Select.Label className="SelectLabel">Meat</Select.Label>
          <SelectItem value="beef">Beef</SelectItem>
          <SelectItem value="chicken">Chicken</SelectItem>
          <SelectItem value="lamb">Lamb</SelectItem>
          <SelectItem value="pork">Pork</SelectItem>
        </Select.Group>
      </Select.Viewport>
      <Select.ScrollDownButton className="SelectScrollButton">
        <ChevronDownIcon />
      </Select.ScrollDownButton>
    </Select.Content>
  </Select.Portal>
</Select.Root>

Source

https://www.radix-ui.com/primitives/docs/components/select

Shadcn

<Select>
  <SelectTrigger className="w-[180px]">
    <SelectValue placeholder="Select a fruit" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Fruits</SelectLabel>
      <SelectItem value="apple">Apple</SelectItem>
      <SelectItem value="banana">Banana</SelectItem>
      <SelectItem value="blueberry">Blueberry</SelectItem>
      <SelectItem value="grapes">Grapes</SelectItem>
      <SelectItem value="pineapple">Pineapple</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Source

https://ui.shadcn.com/docs/components/select

Other Considerations

Since we are not making use of cdkMenu in this proposal and are instead using cdkOverlay directly we may need to handle some issue with opening up or down depending on space. Can probably just look at mat-select for an idea.

Describe any alternatives/workarounds you're currently using

No response

I would be willing to submit a PR to fix this issue

thatsamsonkid commented 11 months ago

Let me revise this after seeing how combobox was made. Can probably reuse parts of that to build a select.

goetzrobin commented 11 months ago

Really like this proposal! I'm also open to reimplementing the Command Dialog in a similar way instead of relying on a 3rd party library like we do now 👀

thatsamsonkid commented 11 months ago

Ok yea awesome we can definitely take a look at that after the select. A lot of the same elements should probably be able to be reused between these 2

goetzrobin commented 11 months ago

I think this is good if you want to take a first pass at implementing this!

thatsamsonkid commented 11 months ago

Awesome yea meant to mention I started already, hoping to have an initial PR for this in next day or two!

badsgahhl commented 8 months ago

As this i merged now could this also be released into the cli? Already waiting for this component but the cli isn't showing it in the latest version.

goetzrobin commented 8 months ago

@badsgahhl I'll try to get a release out with this ASAP