@westpac/ui 0.50.0 → 0.50.3

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.
@@ -1,21 +1,70 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect } from 'react';
2
+ import { create } from 'zustand';
2
3
  import { BREAKPOINTS } from '../tailwind/constants/index.js';
3
4
  function checkBreakpoint() {
5
+ if (typeof window === 'undefined') {
6
+ return 'initial';
7
+ }
4
8
  const breakpointsAsArray = Object.entries(BREAKPOINTS).reverse();
5
9
  const breakpoint = breakpointsAsArray.find(([, value])=>window.matchMedia(`(min-width: ${value})`).matches);
6
10
  return breakpoint ? breakpoint[0] : 'initial';
7
11
  }
12
+ const BREAKPOINTS_ENTRIES = Object.entries(BREAKPOINTS);
13
+ const BREAKPOINTS_MEDIA = BREAKPOINTS_ENTRIES.reduce((acc, [key, value], index)=>{
14
+ const finalValue = (()=>{
15
+ const nextBreakpoint = BREAKPOINTS_ENTRIES[index + 1];
16
+ if (nextBreakpoint) {
17
+ return `(min-width: ${value}) and (max-width: ${+nextBreakpoint[1].replace('px', '') - 1}px)`;
18
+ }
19
+ return `(min-width: ${value})`;
20
+ })();
21
+ return {
22
+ ...acc,
23
+ [key]: finalValue
24
+ };
25
+ }, {
26
+ initial: `(max-width: ${+BREAKPOINTS_ENTRIES[0][1].replace('px', '') - 1}px)`
27
+ });
28
+ const useBreakpointStore = create()((set, get)=>({
29
+ breakpoint: 'initial',
30
+ mediaQueryListeners: null,
31
+ initialised: false,
32
+ ensureInitialized: ()=>{
33
+ if (get().initialised) {
34
+ return;
35
+ }
36
+ const listeners = Object.entries(BREAKPOINTS_MEDIA).map(([label, query])=>{
37
+ const mq = window.matchMedia(query);
38
+ const listener = (e)=>{
39
+ if (e.matches) {
40
+ set({
41
+ breakpoint: label
42
+ });
43
+ }
44
+ };
45
+ mq.addEventListener('change', listener);
46
+ return {
47
+ mq,
48
+ listener
49
+ };
50
+ });
51
+ set({
52
+ mediaQueryListeners: listeners,
53
+ initialised: true,
54
+ breakpoint: checkBreakpoint()
55
+ });
56
+ },
57
+ removeListeners: ()=>{
58
+ var _get_mediaQueryListeners;
59
+ (_get_mediaQueryListeners = get().mediaQueryListeners) === null || _get_mediaQueryListeners === void 0 ? void 0 : _get_mediaQueryListeners.forEach(({ mq, listener })=>{
60
+ mq.removeEventListener('change', listener);
61
+ });
62
+ }
63
+ }));
8
64
  export function useBreakpoint() {
9
- const [breakpoint, setBreakpoint] = useState(checkBreakpoint());
65
+ const { breakpoint, ensureInitialized: initIfNotInitialised } = useBreakpointStore();
10
66
  useEffect(()=>{
11
- const listener = ()=>{
12
- const breakpoint = checkBreakpoint();
13
- setBreakpoint(breakpoint);
14
- };
15
- window.addEventListener('resize', listener);
16
- return ()=>{
17
- window.removeEventListener('resize', listener);
18
- };
67
+ initIfNotInitialised();
19
68
  }, []);
20
69
  return breakpoint;
21
70
  }
@@ -1 +1,2 @@
1
+ export * from './breakpoints.hook.js';
1
2
  export * from '../components/pagination/pagination.hooks.js';
@@ -1 +1,2 @@
1
+ export * from './breakpoints.hook.js';
1
2
  export * from '../components/pagination/pagination.hooks.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westpac/ui",
3
- "version": "0.50.0",
3
+ "version": "0.50.3",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -270,7 +270,8 @@
270
270
  "lodash.throttle": "~4.1.1",
271
271
  "motion": "~12.23.12",
272
272
  "react-aria": "~3.41.1",
273
- "react-stately": "~3.39.0"
273
+ "react-stately": "~3.39.0",
274
+ "zustand": "~4.5.4"
274
275
  },
275
276
  "overrides": {
276
277
  "react-aria": {
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable sonarjs/deprecation */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
3
  import { useAccordionItem } from '@react-aria/accordion';
4
- import { AnimatePresence, LazyMotion, m } from 'motion/react';
5
- import React, { useRef } from 'react';
4
+ import { LazyMotion, m, useAnimate } from 'motion/react';
5
+ import React, { useEffect, useRef, useState } from 'react';
6
6
  import { mergeProps, useFocusRing, useHover, useLocale } from 'react-aria';
7
7
 
8
8
  import { ArrowLeftIcon, ArrowRightIcon } from '../../../icon/index.js';
@@ -23,11 +23,44 @@ export function AccordionItem<T = HTMLElement>({
23
23
  const { state, item } = props;
24
24
  const { buttonProps, regionProps } = useAccordionItem<T>(props, state, ref);
25
25
  const { isFocusVisible, focusProps } = useFocusRing();
26
- const isOpen = state.expandedKeys.has(item.key);
26
+ const isOpenState = state.expandedKeys.has(item.key);
27
27
  const isDisabled = state.disabledKeys.has(item.key);
28
28
  const { hoverProps } = useHover({ isDisabled });
29
29
  const { direction } = useLocale();
30
- const styles = accordionItemStyles({ isOpen, isDisabled, look, isFocusVisible, rounded });
30
+ // Open/close animation needs to be done with useAnimate as AnimatePresence will unmount component
31
+ const [scope, animate] = useAnimate();
32
+ const [enableOpenStyle, setEnableOpenStyle] = useState(false);
33
+
34
+ const styles = accordionItemStyles({
35
+ isOpen: enableOpenStyle,
36
+ isOpenState,
37
+ isDisabled,
38
+ look,
39
+ isFocusVisible,
40
+ rounded,
41
+ });
42
+
43
+ useEffect(() => {
44
+ // setEnableStyle here as opening animation isn't working correctly if done below
45
+ if (isOpenState) setEnableOpenStyle(true);
46
+
47
+ if (enableOpenStyle) {
48
+ animate(scope.current, { height: 'auto' }, { duration: 0.3, ease: [0.25, 0.1, 0.25, 1.0] });
49
+ }
50
+ if (!isOpenState) {
51
+ animate(
52
+ scope.current,
53
+ { height: '0px' },
54
+ {
55
+ duration: 0.3,
56
+ ease: [0.25, 0.1, 0.25, 1.0],
57
+ // set some styles after animation completes so content doesn't disappear on close
58
+ onComplete: () => setEnableOpenStyle(false),
59
+ },
60
+ );
61
+ }
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [isOpenState, enableOpenStyle]);
31
64
 
32
65
  return (
33
66
  <Tag className={styles.base({ className })}>
@@ -47,33 +80,18 @@ export function AccordionItem<T = HTMLElement>({
47
80
  </h3>
48
81
  <div {...regionProps}>
49
82
  <LazyMotion features={loadAnimations}>
50
- <AnimatePresence initial={false}>
51
- <m.div
52
- className="overflow-hidden"
53
- initial={{
54
- height: 0,
55
- }}
56
- animate={{
57
- height: 'auto',
58
- }}
59
- exit={{
60
- height: 0,
83
+ <m.div className="overflow-hidden" initial={{ height: isOpenState ? 'auto' : 0 }} ref={scope}>
84
+ <div
85
+ className={styles.content()}
86
+ // TODO: Remove below with updated accordion that uses disclosure as the issue doesn't happen with that version
87
+ // Need to call stopPropagation here as some events from children are bubbling up and focusing the accordion i.e. inputs
88
+ onBlur={e => {
89
+ e.stopPropagation();
61
90
  }}
62
- transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1.0] }}
63
- key={`${item.index}-${isOpen}`}
64
91
  >
65
- <div
66
- className={styles.content()}
67
- // TODO: Remove below with updated accordion that uses disclosure as the issue doesn't happen with that version
68
- // Need to call stopPropagation here as some events from children are bubbling up and focusing the accordion i.e. inputs
69
- onBlur={e => {
70
- e.stopPropagation();
71
- }}
72
- >
73
- {item.props.children}
74
- </div>
75
- </m.div>
76
- </AnimatePresence>
92
+ {item.props.children}
93
+ </div>
94
+ </m.div>
77
95
  </LazyMotion>
78
96
  </div>
79
97
  </Tag>
@@ -7,7 +7,7 @@ export const styles = tv(
7
7
  itemHeader: 'typography-body-9 flex w-full flex-1 items-center justify-between px-3 py-2 group-first:border-t-0',
8
8
  headerTitleWrapper: 'flex-1 pr-2 text-left',
9
9
  indicator: 'size-3 rotate-90',
10
- content: 'hidden',
10
+ content: '',
11
11
  },
12
12
  variants: {
13
13
  look: {
@@ -19,13 +19,17 @@ export const styles = tv(
19
19
  'mb-[-1px] border-l-[0.375rem] border-r border-border bg-light shadow-[inset_0px_1px_0_0_var(--tw-shadow-color),inset_0_-1px_0_0_var(--tw-shadow-color)] !shadow-border transition-colors',
20
20
  },
21
21
  },
22
+ isOpenState: {
23
+ false: {
24
+ itemHeader: 'background-transition hover:bg-background',
25
+ },
26
+ },
22
27
  isOpen: {
23
28
  true: {
24
- content: 'block border-border p-3',
29
+ content: 'visible block border-border p-3',
25
30
  },
26
31
  false: {
27
- base: '',
28
- itemHeader: 'background-transition hover:bg-background',
32
+ content: 'hidden',
29
33
  },
30
34
  },
31
35
  isDisabled: {
@@ -45,13 +49,13 @@ export const styles = tv(
45
49
  compoundSlots: [
46
50
  {
47
51
  slots: ['indicator'],
48
- isOpen: true,
52
+ isOpenState: true,
49
53
  className: '-rotate-90',
50
54
  },
51
55
  {
52
56
  slots: ['itemHeader'],
53
57
  look: 'lego',
54
- isOpen: true,
58
+ isOpenState: true,
55
59
  className: 'border-l-hero',
56
60
  },
57
61
  {
@@ -41,13 +41,15 @@ function Autocomplete<T extends object>(
41
41
  className,
42
42
  width = 'full',
43
43
  loadingState,
44
+ comboBoxState,
44
45
  ...props
45
46
  }: AutocompleteProps<T>,
46
47
  ref: ForwardedRef<HTMLInputElement>,
47
48
  ) {
48
49
  // eslint-disable-next-line @typescript-eslint/unbound-method
49
50
  const { contains } = useFilter({ sensitivity: 'base' });
50
- const state = useComboBoxState({ isDisabled, ...props, defaultFilter: contains });
51
+ const internalState = useComboBoxState({ isDisabled, ...props, defaultFilter: contains });
52
+ const state = comboBoxState ?? internalState;
51
53
  const { isFocusVisible, focusProps } = useFocusRing();
52
54
  const { isFocusVisible: isInputFocusVisible, focusProps: inputFocusProps } = useFocusRing();
53
55
  const inputRef = React.useRef<HTMLInputElement>(null);
@@ -1,6 +1,7 @@
1
1
  import { type ComboBoxProps } from '@react-types/combobox';
2
2
  import { type AriaLabelingProps } from '@react-types/shared';
3
3
  import { type ReactNode } from 'react';
4
+ import { ComboBoxState } from 'react-stately';
4
5
  import { type VariantProps } from 'tailwind-variants';
5
6
 
6
7
  import { HintProps, InputProps } from '../index.js';
@@ -63,5 +64,10 @@ export type AutocompleteProps<T extends object> = {
63
64
  * Width of autocomplete
64
65
  */
65
66
  width?: InputProps['width'];
67
+ /**
68
+ * Pass through comboBox state from consuming component. If not specified,
69
+ * will be handled internally.
70
+ */
71
+ comboBoxState?: ComboBoxState<T>;
66
72
  } & ComboBoxProps<T> &
67
73
  AriaLabelingProps;
@@ -3,7 +3,7 @@
3
3
  import React from 'react';
4
4
  import { useFocusRing } from 'react-aria';
5
5
 
6
- import { Grid, GridItem, VisuallyHidden } from '../index.js';
6
+ import { VisuallyHidden } from '../index.js';
7
7
  import {
8
8
  BOMMultibrandSmallLogo,
9
9
  BSAMultibrandSmallLogo,
@@ -54,18 +54,14 @@ export function Footer({
54
54
  return (
55
55
  <footer className={styles.base({ className })} {...props}>
56
56
  <div className={styles.wrapper()}>
57
- <Grid className={styles.topRow()}>
58
- <GridItem span={12}>{children}</GridItem>
59
- </Grid>
57
+ <div>{children}</div>
60
58
  {!hideLogo && (
61
- <Grid>
62
- <GridItem span={{ initial: 12, md: 1 }}>
63
- <a href={logoLink} className={styles.link()} {...focusProps}>
64
- {srOnlyText && <VisuallyHidden>{srOnlyText}</VisuallyHidden>}
65
- <Logo align="right" aria-label={logoAssistiveText} />
66
- </a>
67
- </GridItem>
68
- </Grid>
59
+ <div className={styles.logoWrapper()}>
60
+ <a href={logoLink} className={styles.link()} {...focusProps}>
61
+ {srOnlyText && <VisuallyHidden>{srOnlyText}</VisuallyHidden>}
62
+ <Logo align="right" aria-label={logoAssistiveText} />
63
+ </a>
64
+ </div>
69
65
  )}
70
66
  </div>
71
67
  </footer>
@@ -4,9 +4,9 @@ export const styles = tv(
4
4
  {
5
5
  slots: {
6
6
  base: 'relative overflow-hidden border-t border-t-border',
7
- wrapper: 'pt-3 max-md:px-2 max-md:pb-3 md:px-4 md:pb-4',
8
- topRow: '',
7
+ wrapper: 'pt-3 max-md:px-2 max-md:pb-3 md:px-4 md:pb-3',
9
8
  link: 'float-right block',
9
+ logoWrapper: 'flex justify-end',
10
10
  },
11
11
  variants: {
12
12
  offsetSidebar: {
@@ -18,10 +18,6 @@ export const styles = tv(
18
18
  isFocusVisible: {
19
19
  true: { link: 'focus-outline' },
20
20
  },
21
- hideLogo: {
22
- true: '',
23
- false: { topRow: 'max-md:mb-7 md:mb-4' },
24
- },
25
21
  },
26
22
  },
27
23
  { responsiveVariants: ['xsl', 'sm', 'md', 'lg', 'xl'] },
@@ -1,8 +1,12 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect } from 'react';
2
+ import { create } from 'zustand';
2
3
 
3
4
  import { BREAKPOINTS, Breakpoint } from '../tailwind/constants/index.js';
4
5
 
5
6
  function checkBreakpoint(): Breakpoint | 'initial' {
7
+ if (typeof window === 'undefined') {
8
+ return 'initial';
9
+ }
6
10
  const breakpointsAsArray = Object.entries(BREAKPOINTS).reverse() as [Breakpoint, string][];
7
11
  const breakpoint = breakpointsAsArray.find(([, value]) => window.matchMedia(`(min-width: ${value})`).matches) as [
8
12
  Breakpoint,
@@ -11,18 +15,74 @@ function checkBreakpoint(): Breakpoint | 'initial' {
11
15
  return breakpoint ? breakpoint[0] : 'initial';
12
16
  }
13
17
 
14
- export function useBreakpoint() {
15
- const [breakpoint, setBreakpoint] = useState<Breakpoint | 'initial'>(checkBreakpoint());
18
+ const BREAKPOINTS_ENTRIES = Object.entries(BREAKPOINTS);
19
+ const BREAKPOINTS_MEDIA: Record<Breakpoint | 'initial', string> = BREAKPOINTS_ENTRIES.reduce(
20
+ (acc, [key, value], index) => {
21
+ const finalValue = (() => {
22
+ const nextBreakpoint = BREAKPOINTS_ENTRIES[index + 1];
23
+ if (nextBreakpoint) {
24
+ return `(min-width: ${value}) and (max-width: ${+nextBreakpoint[1].replace('px', '') - 1}px)`;
25
+ }
26
+ return `(min-width: ${value})`;
27
+ })();
16
28
 
17
- useEffect(() => {
18
- const listener = () => {
19
- const breakpoint = checkBreakpoint();
20
- setBreakpoint(breakpoint);
21
- };
22
- window.addEventListener('resize', listener);
23
- return () => {
24
- window.removeEventListener('resize', listener);
29
+ return {
30
+ ...acc,
31
+ [key]: finalValue,
25
32
  };
33
+ },
34
+ {
35
+ initial: `(max-width: ${+BREAKPOINTS_ENTRIES[0][1].replace('px', '') - 1}px)`,
36
+ } as Record<Breakpoint | 'initial', string>,
37
+ );
38
+
39
+ type BreakpointState = {
40
+ breakpoint: Breakpoint | 'initial';
41
+ mediaQueryListeners:
42
+ | {
43
+ mq: MediaQueryList;
44
+ listener: (e: MediaQueryListEvent) => void;
45
+ }[]
46
+ | null;
47
+ initialised: boolean;
48
+ ensureInitialized: () => void;
49
+ removeListeners: () => void;
50
+ };
51
+
52
+ const useBreakpointStore = create<BreakpointState>()((set, get) => ({
53
+ breakpoint: 'initial',
54
+ mediaQueryListeners: null,
55
+ initialised: false,
56
+ ensureInitialized: () => {
57
+ if (get().initialised) {
58
+ return;
59
+ }
60
+ const listeners = Object.entries(BREAKPOINTS_MEDIA).map(([label, query]) => {
61
+ const mq = window.matchMedia(query);
62
+ const listener = (e: MediaQueryListEvent) => {
63
+ if (e.matches) {
64
+ set({ breakpoint: label as Breakpoint });
65
+ }
66
+ };
67
+ mq.addEventListener('change', listener);
68
+ return {
69
+ mq,
70
+ listener,
71
+ };
72
+ });
73
+ set({ mediaQueryListeners: listeners, initialised: true, breakpoint: checkBreakpoint() });
74
+ },
75
+ removeListeners: () => {
76
+ get().mediaQueryListeners?.forEach(({ mq, listener }) => {
77
+ mq.removeEventListener('change', listener);
78
+ });
79
+ },
80
+ }));
81
+
82
+ export function useBreakpoint() {
83
+ const { breakpoint, ensureInitialized: initIfNotInitialised } = useBreakpointStore();
84
+ useEffect(() => {
85
+ initIfNotInitialised();
26
86
  }, []);
27
87
 
28
88
  return breakpoint;
package/src/hook/index.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from './breakpoints.hook.js';
1
2
  export * from '../components/pagination/pagination.hooks.js';