@westpac/ui 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.agents/skills/creating-gel-component/SKILL.md +13 -9
  2. package/.agents/skills/reviewing-gel-component/SKILL.md +19 -9
  3. package/.agents/skills/writing-gel-tests/SKILL.md +10 -6
  4. package/CHANGELOG.md +19 -0
  5. package/dist/component-type.json +1 -1
  6. package/dist/components/autocomplete/autocomplete.component.js +2 -2
  7. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.d.ts +1 -1
  8. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.js +2 -2
  9. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.d.ts +1 -0
  10. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.js +2 -2
  11. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.d.ts +1 -1
  12. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.js +3 -2
  13. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.d.ts +42 -39
  14. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.js +15 -14
  15. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.d.ts +1 -0
  16. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.js +19 -6
  17. package/dist/components/multi-select/multi-select.component.d.ts +1 -1
  18. package/dist/components/multi-select/multi-select.component.js +19 -6
  19. package/dist/components/multi-select/multi-select.types.d.ts +22 -1
  20. package/package.json +4 -4
  21. package/src/components/autocomplete/autocomplete.component.tsx +2 -2
  22. package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.tsx +4 -1
  23. package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.ts +1 -0
  24. package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.tsx +2 -2
  25. package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.tsx +2 -0
  26. package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.ts +14 -14
  27. package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.ts +1 -0
  28. package/src/components/multi-select/components/multi-select-popover/multi-select-popover.component.tsx +32 -5
  29. package/src/components/multi-select/multi-select.component.tsx +18 -3
  30. package/src/components/multi-select/multi-select.types.ts +23 -1
@@ -2,7 +2,7 @@ import { tv } from 'tailwind-variants';
2
2
 
3
3
  export const styles = tv({
4
4
  slots: {
5
- buttonContainer: 'relative w-full',
5
+ buttonContainer: 'relative w-fit',
6
6
  control: 'form-control relative box-border inline-flex w-full flex-row overflow-hidden',
7
7
  selection: 'flex flex-1 items-center overflow-hidden pr-4.5 text-left whitespace-nowrap',
8
8
  selectionSpan: 'w-full overflow-hidden text-ellipsis',
@@ -52,19 +52,19 @@ export const styles = tv({
52
52
  },
53
53
  },
54
54
  width: {
55
- full: { control: 'w-full' },
56
- 1: { control: 'box-content w-[1.81ex]' },
57
- 2: { control: 'box-content w-[3.62ex]' },
58
- 3: { control: 'box-content w-[5.43ex]' },
59
- 4: { control: 'box-content w-[7.24ex]' },
60
- 5: { control: 'box-content w-[9.05ex]' },
61
- 6: { control: 'box-content w-[10.86ex]' },
62
- 7: { control: 'box-content w-[12.67ex]' },
63
- 8: { control: 'box-content w-[14.48ex]' },
64
- 9: { control: 'box-content w-[16.29ex]' },
65
- 10: { control: 'box-content w-[18.1ex]' },
66
- 20: { control: 'box-content w-[36.2ex]' },
67
- 30: { control: 'box-content w-[54.3ex]' },
55
+ full: { selectionSpan: 'w-full', buttonContainer: 'w-full' },
56
+ 1: { selectionSpan: 'w-[1.81ex]' },
57
+ 2: { selectionSpan: 'w-[3.62ex]' },
58
+ 3: { selectionSpan: 'w-[5.43ex]' },
59
+ 4: { selectionSpan: 'w-[7.24ex]' },
60
+ 5: { selectionSpan: 'w-[9.05ex]' },
61
+ 6: { selectionSpan: 'w-[10.86ex]' },
62
+ 7: { selectionSpan: 'w-[12.67ex]' },
63
+ 8: { selectionSpan: 'w-[14.48ex]' },
64
+ 9: { selectionSpan: 'w-[16.29ex]' },
65
+ 10: { selectionSpan: 'w-[18.1ex]' },
66
+ 20: { selectionSpan: 'w-[36.2ex]' },
67
+ 30: { selectionSpan: 'w-[54.3ex]' },
68
68
  },
69
69
  },
70
70
  });
@@ -18,4 +18,5 @@ export type MultiSelectListBoxTriggerProps<T> = {
18
18
  selectedKeys?: ListProps<T>['selectedKeys'];
19
19
  showSingleSectionTitle?: MultiSelectProps<T>['showSingleSectionTitle'];
20
20
  triggerProps: AriaButtonProps<'button'>;
21
+ width?: ResponsiveVariants<Variants['width']>;
21
22
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import React, { useContext } from 'react';
3
+ import React, { useContext, useLayoutEffect, useMemo } from 'react';
4
4
  import { DismissButton, mergeProps, Overlay, usePopover } from 'react-aria';
5
5
 
6
6
  import { MultiSelectContext } from '../../multi-select.component.js';
@@ -13,10 +13,21 @@ export function MultiSelectPopover({ children, className, ...props }: MultiSelec
13
13
  const { overlayState, overlayProps, popoverRef, buttonRef, placement, portalContainer } =
14
14
  useContext(MultiSelectContext);
15
15
 
16
+ const [isPopoverSmaller, setIsPopoverSmaller] = React.useState(false);
17
+
18
+ useLayoutEffect(() => {
19
+ if (buttonRef.current && popoverRef.current) {
20
+ const buttonWidth = buttonRef.current.getBoundingClientRect().width;
21
+ const popoverWidth = popoverRef.current.getBoundingClientRect().width;
22
+ setIsPopoverSmaller(popoverWidth < buttonWidth);
23
+ }
24
+ // eslint-disable-next-line react-hooks/exhaustive-deps
25
+ }, []);
26
+
16
27
  const { popoverProps } = usePopover(
17
28
  {
18
29
  ...props,
19
- placement,
30
+ placement: placement,
20
31
  popoverRef,
21
32
  triggerRef: buttonRef,
22
33
  isNonModal: true,
@@ -27,16 +38,32 @@ export function MultiSelectPopover({ children, className, ...props }: MultiSelec
27
38
  overlayState,
28
39
  );
29
40
 
30
- const width = buttonRef.current?.getBoundingClientRect().width;
41
+ // This is required so branding applies correctly by default due to portal location, can be overridden with portalContainer prop
42
+ const brandContainer = useMemo(() => {
43
+ if (typeof window !== 'undefined') {
44
+ return (
45
+ document.querySelector('[data-theme]') ||
46
+ document.querySelector('[class^="theme-"], [class*=" theme-"]') ||
47
+ document.body
48
+ );
49
+ }
50
+ }, []);
51
+
31
52
  const styles = popoverStyles();
32
53
 
33
54
  return (
34
- <Overlay disableFocusManagement portalContainer={portalContainer}>
55
+ <Overlay disableFocusManagement portalContainer={portalContainer || brandContainer}>
35
56
  <div
36
57
  {...mergeProps(popoverProps, overlayProps)}
37
58
  ref={popoverRef}
38
59
  className={styles.overlay({ className })}
39
- style={{ ...popoverProps.style, width: width ? `${width}px` : undefined }}
60
+ style={{
61
+ ...popoverProps.style,
62
+ width: isPopoverSmaller ? buttonRef.current?.getBoundingClientRect().width : undefined,
63
+ maxWidth: brandContainer
64
+ ? brandContainer.getBoundingClientRect().width - (parseInt(popoverProps.style?.left as string) || 0)
65
+ : undefined,
66
+ }}
40
67
  onBlur={e => {
41
68
  const related = e.relatedTarget as Element | null;
42
69
  if (!popoverRef?.current) return;
@@ -27,6 +27,7 @@ export const MultiSelectContext = createContext<MultiSelectContextProps>({
27
27
  inputRef: { current: null },
28
28
  filterText: '',
29
29
  overlayProps: {},
30
+ hideSelectAll: false,
30
31
  });
31
32
 
32
33
  export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
@@ -37,9 +38,12 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
37
38
  onSelectionChange,
38
39
  placeholder = 'Select',
39
40
  showSingleSectionTitle = false,
40
- placement,
41
+ placement = 'bottom left',
41
42
  portalContainer,
42
43
  id,
44
+ hideFilter = false,
45
+ hideSelectAll = false,
46
+ width = 'full',
43
47
  ...props
44
48
  }: MultiSelectProps<T>) {
45
49
  const [filterText, setFilterText] = useState('');
@@ -67,7 +71,14 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
67
71
  onOpenChange: isOpen => {
68
72
  if (isOpen) {
69
73
  requestAnimationFrame(() => {
70
- inputRef.current?.focus();
74
+ if (!hideFilter) {
75
+ inputRef.current?.focus();
76
+ } else if (selectionMode === 'multiple' && !hideSelectAll) {
77
+ selectAllRef.current?.focus();
78
+ } else {
79
+ const firstItem = listBoxRef.current?.querySelector('[data-key]') as HTMLElement;
80
+ firstItem?.focus();
81
+ }
71
82
  });
72
83
  }
73
84
  if (!isOpen) {
@@ -94,6 +105,7 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
94
105
  inputRef,
95
106
  overlayProps,
96
107
  portalContainer,
108
+ hideSelectAll,
97
109
  }}
98
110
  >
99
111
  <div className={styles.root()}>
@@ -103,8 +115,11 @@ export function BaseMultiSelect<T extends MultiSelectValue = MultiSelectValue>({
103
115
  showSingleSectionTitle={showSingleSectionTitle}
104
116
  triggerProps={triggerProps}
105
117
  id={id}
118
+ width={width}
106
119
  />
107
- {overlayState.isOpen && <MultiSelectDropdown setFilterText={setFilterText} {...listBoxProps} />}
120
+ {overlayState.isOpen && (
121
+ <MultiSelectDropdown setFilterText={setFilterText} hideFilter={hideFilter} {...listBoxProps} />
122
+ )}
108
123
  </div>
109
124
  </MultiSelectContext.Provider>
110
125
  );
@@ -2,9 +2,15 @@ import { DOMProps } from '@react-types/shared';
2
2
  import { Key, ReactNode, RefObject } from 'react';
3
3
  import { AriaListBoxOptions, AriaPopoverProps } from 'react-aria';
4
4
  import { ItemProps, ListProps, ListState, OverlayTriggerState } from 'react-stately';
5
+ import { VariantProps } from 'tailwind-variants';
5
6
 
7
+ import { ResponsiveVariants } from '../../types/responsive-variants.types.js';
8
+
9
+ import { styles as triggerStyles } from './components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.js';
6
10
  import { MultiSelectSize } from './components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.js';
7
11
 
12
+ type Variants = VariantProps<typeof triggerStyles>;
13
+
8
14
  export type MultiSelectContextProps<T extends object = object> = {
9
15
  size?: MultiSelectSize;
10
16
  overlayState: OverlayTriggerState;
@@ -18,6 +24,7 @@ export type MultiSelectContextProps<T extends object = object> = {
18
24
  overlayProps: DOMProps;
19
25
  placement?: AriaPopoverProps['placement'];
20
26
  portalContainer?: Element;
27
+ hideSelectAll?: boolean;
21
28
  };
22
29
 
23
30
  export type MultiSelectItemProps<T extends object = object> = { description?: string } & ItemProps<T>;
@@ -26,6 +33,16 @@ export type MultiSelectItemProps<T extends object = object> = { description?: st
26
33
  export type MultiSelectValue = { textValue?: string; content?: ReactNode; key: Key; description?: string };
27
34
 
28
35
  export type MultiSelectProps<T> = {
36
+ /**
37
+ * Whether to hide the filter input in the dropdown
38
+ * @default false
39
+ */
40
+ hideFilter?: boolean;
41
+ /**
42
+ * Whether to hide the "Select All" option in the dropdown for multiple selection multi-selects
43
+ * @default false
44
+ */
45
+ hideSelectAll?: boolean;
29
46
  /**
30
47
  * Props for the list box within the multi-select
31
48
  */
@@ -40,7 +57,7 @@ export type MultiSelectProps<T> = {
40
57
  placeholder?: string;
41
58
  /**
42
59
  * Manual placement of the dropdown, will flip automatically if there is not enough space
43
- * @default bottom
60
+ * @default 'bottom left'
44
61
  */
45
62
  placement?: AriaPopoverProps['placement'];
46
63
  /**
@@ -57,4 +74,9 @@ export type MultiSelectProps<T> = {
57
74
  * @default medium
58
75
  */
59
76
  size?: MultiSelectSize;
77
+ /**
78
+ * Width of the multi-select, can be a fixed width or full width
79
+ * @default full
80
+ */
81
+ width?: ResponsiveVariants<Variants['width']>;
60
82
  } & ListProps<T>;