Skip to content

Commit

Permalink
Merge branch 'refs/heads/master' into HEAD
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] committed Dec 18, 2024
2 parents 766a65a + 2af1c20 commit 7e62b7f
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
type ReactNode,
useCallback,
} from 'react'
import { tv } from 'tailwind-variants'

import { Button } from '../../Button'
import { Cluster, Stack } from '../../Layout'
Expand Down Expand Up @@ -46,6 +47,17 @@ export type FormDialogContentInnerProps = BaseProps & {

const CLOSE_BUTTON_LABEL = 'キャンセル'

const formDialogContentInner = tv({
extend: dialogContentInner,
slots: {
// 領域を狭くしたときにwrapperも縮むようにflexを使用
wrapper: 'shr-flex shr-flex-col',
// 領域を狭くしたときにwrapperも縮むようにflexを使用
form: 'shr-overflow-y-auto shr-flex-auto shr-flex shr-flex-col',
contentWrapper: 'shr-overflow-y-auto shr-flex-auto',
},
})

export const FormDialogContentInner: FC<FormDialogContentInnerProps> = ({
children,
title,
Expand Down Expand Up @@ -76,48 +88,49 @@ export const FormDialogContentInner: FC<FormDialogContentInnerProps> = ({
)
const isRequestProcessing = responseMessage && responseMessage.status === 'processing'

const { wrapper, actionArea, buttonArea, message } = dialogContentInner()
const { form, contentWrapper, wrapper, actionArea, buttonArea, message } =
formDialogContentInner()

return (
// eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content, smarthr/a11y-prohibit-sectioning-content-in-form
<Section>
<Section className={wrapper()}>
<DialogHeader title={title} subtitle={subtitle} titleTag={titleTag} titleId={titleId} />
<form onSubmit={handleSubmitAction}>
<div className={wrapper()}>
<form onSubmit={handleSubmitAction} className={form()}>
<div className={contentWrapper()}>
<DialogBody contentPadding={contentPadding} contentBgColor={contentBgColor}>
{children}
</DialogBody>
<Stack gap={0.5} className={actionArea()}>
<Cluster justify="space-between">
{subActionArea}
<Cluster gap={{ row: 0.5, column: 1 }} className={buttonArea()}>
<Button
onClick={onClickClose}
disabled={closeDisabled || isRequestProcessing}
className="smarthr-ui-Dialog-closeButton"
>
{decorators?.closeButtonLabel?.(CLOSE_BUTTON_LABEL) || CLOSE_BUTTON_LABEL}
</Button>
<Button
type="submit"
variant={actionTheme}
disabled={actionDisabled}
loading={isRequestProcessing}
className="smarthr-ui-Dialog-actionButton"
>
{actionText}
</Button>
</Cluster>
</Cluster>
{(responseMessage?.status === 'success' || responseMessage?.status === 'error') && (
<div className={message()}>
<ResponseMessage type={responseMessage.status} role="alert">
{responseMessage.text}
</ResponseMessage>
</div>
)}
</Stack>
</div>
<Stack gap={0.5} className={actionArea()}>
<Cluster justify="space-between">
{subActionArea}
<Cluster gap={{ row: 0.5, column: 1 }} className={buttonArea()}>
<Button
onClick={onClickClose}
disabled={closeDisabled || isRequestProcessing}
className="smarthr-ui-Dialog-closeButton"
>
{decorators?.closeButtonLabel?.(CLOSE_BUTTON_LABEL) || CLOSE_BUTTON_LABEL}
</Button>
<Button
type="submit"
variant={actionTheme}
disabled={actionDisabled}
loading={isRequestProcessing}
className="smarthr-ui-Dialog-actionButton"
>
{actionText}
</Button>
</Cluster>
</Cluster>
{(responseMessage?.status === 'success' || responseMessage?.status === 'error') && (
<div className={message()}>
<ResponseMessage type={responseMessage.status} role="alert">
{responseMessage.text}
</ResponseMessage>
</div>
)}
</Stack>
</form>
</Section>
)
Expand Down
27 changes: 26 additions & 1 deletion packages/smarthr-ui/src/components/Dialog/ModelessDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export const ModelessDialog: FC<Props & BaseElementProps & VariantProps<typeof m
contentPadding,
footer,
isOpen,
onClickClose,
onPressEscape,
resizable = false,
width,
Expand All @@ -143,6 +142,7 @@ export const ModelessDialog: FC<Props & BaseElementProps & VariantProps<typeof m
...props
}) => {
const labelId = useId()
const lastFocusElementRef = useRef<HTMLElement | null>(null)
const { createPortal } = useDialogPortal(portalParent, id)

const {
Expand Down Expand Up @@ -310,14 +310,39 @@ export const ModelessDialog: FC<Props & BaseElementProps & VariantProps<typeof m
}
}, [isOpen])

const onClickClose = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
lastFocusElementRef.current?.focus()
props.onClickClose?.(e)
},
[props],
)

useHandleEscape(
useCallback(() => {
if (isOpen && onPressEscape) {
lastFocusElementRef.current?.focus()
onPressEscape()
}
}, [isOpen, onPressEscape]),
)

useEffect(() => {
const focusHandler = (e: FocusEvent) => {
if (!(e.target instanceof HTMLElement)) return

// e.target(現在フォーカスがあたっている要素)がModeless dialogの中の要素であれば、lastFocusElementRefに代入しない
if (wrapperRef?.current?.contains(e.target)) {
return
}

lastFocusElementRef.current = e.target
}

document.addEventListener('focus', focusHandler, true)
return () => document.removeEventListener('focus', focusHandler, true)
}, [])

return createPortal(
<DialogOverlap isOpen={isOpen} className={overlapStyle}>
<Draggable
Expand Down
120 changes: 95 additions & 25 deletions packages/smarthr-ui/src/components/Textarea/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { tv } from 'tailwind-variants'
import { debounce } from '../../libs/debounce'
import { lineHeight } from '../../themes'
import { defaultHtmlFontSize } from '../../themes/createFontSize'
import { VisuallyHiddenText } from '../VisuallyHiddenText'

import type { DecoratorsType } from '../../types'

Expand All @@ -36,7 +37,13 @@ type Props = {
/** 入力可能な最大文字数。あと何文字入力できるかの表示が追加される。html的なvalidateは発生しない */
maxLetters?: number
/** コンポーネント内の文言を変更するための関数を設定 */
decorators?: DecoratorsType<'beforeMaxLettersCount' | 'afterMaxLettersCount'>
decorators?: DecoratorsType<
| 'beforeMaxLettersCount'
| 'afterMaxLettersCount'
| 'afterMaxLettersCountExceeded'
| 'beforeScreenReaderMaxLettersDescription'
| 'afterScreenReaderMaxLettersDescription'
>
/**
* @deprecated placeholder属性は非推奨です。別途ヒント用要素の設置を検討してください。
*/
Expand All @@ -59,6 +66,10 @@ const getStringLength = (value: string | number | readonly string[]) => {

const TEXT_BEFORE_MAXLETTERS_COUNT = 'あと'
const TEXT_AFTER_MAXLETTERS_COUNT = '文字'
const TEXT_AFTER_MAXLETTERS_COUNT_EXCEEDED = 'オーバー'

const SCREEN_READER_BEFORE_MAXLETTERS_DESCRIPTION = '最大'
const SCREEN_READER_AFTER_MAXLETTERS_DESCRIPTION = '文字入力できます'

const textarea = tv({
slots: {
Expand All @@ -72,16 +83,13 @@ const textarea = tv({
'aria-[invalid]:shr-border-danger',
],
counter: 'smarthr-ui-Textarea-counter shr-block shr-text-sm',
counterText: 'shr-font-bold',
counterText: 'shr-text-black',
},
variants: {
error: {
true: {
counterText: 'shr-text-danger',
},
false: {
counterText: 'shr-text-grey',
},
},
},
defaultVariants: {
Expand All @@ -108,25 +116,61 @@ export const Textarea = forwardRef<HTMLTextAreaElement, Props & ElementProps>(
ref,
) => {
const maxLettersId = useId()
const maxLettersNoticeId = `${maxLettersId}-notice`
const actualMaxLettersId = maxLetters ? maxLettersId : undefined

const textareaRef = useRef<HTMLTextAreaElement>(null)
const currentValue = props.defaultValue || props.value
const [interimRows, setInterimRows] = useState(rows)
const [count, setCount] = useState(currentValue ? getStringLength(currentValue) : 0)
const beforeMaxLettersCount = useMemo(
() =>
decorators?.beforeMaxLettersCount?.(TEXT_BEFORE_MAXLETTERS_COUNT) ||
TEXT_BEFORE_MAXLETTERS_COUNT,
const [srCounterMessage, setSrCounterMessage] = useState('')

const {
afterMaxLettersCount,
beforeMaxLettersCount,
maxLettersCountExceeded,
beforeScreenReaderMaxLettersDescription,
afterScreenReaderMaxLettersDescription,
} = useMemo(
() => ({
beforeMaxLettersCount:
decorators?.beforeMaxLettersCount?.(TEXT_BEFORE_MAXLETTERS_COUNT) ||
TEXT_BEFORE_MAXLETTERS_COUNT,
afterMaxLettersCount:
decorators?.afterMaxLettersCount?.(TEXT_AFTER_MAXLETTERS_COUNT) ||
TEXT_AFTER_MAXLETTERS_COUNT,
maxLettersCountExceeded:
decorators?.afterMaxLettersCountExceeded?.(TEXT_AFTER_MAXLETTERS_COUNT_EXCEEDED) ||
TEXT_AFTER_MAXLETTERS_COUNT_EXCEEDED,
beforeScreenReaderMaxLettersDescription:
decorators?.beforeScreenReaderMaxLettersDescription?.(
SCREEN_READER_BEFORE_MAXLETTERS_DESCRIPTION,
) || SCREEN_READER_BEFORE_MAXLETTERS_DESCRIPTION,
afterScreenReaderMaxLettersDescription:
decorators?.afterScreenReaderMaxLettersDescription?.(
SCREEN_READER_AFTER_MAXLETTERS_DESCRIPTION,
) || SCREEN_READER_AFTER_MAXLETTERS_DESCRIPTION,
}),
[decorators],
)
const afterMaxLettersCount = useMemo(
() =>
decorators?.afterMaxLettersCount?.(TEXT_AFTER_MAXLETTERS_COUNT) ||
TEXT_AFTER_MAXLETTERS_COUNT,
[decorators],

const getCounterMessage = useCallback(
(counterValue: number) => {
if (maxLetters === undefined) return

if (counterValue > maxLetters) {
// {count}文字オーバー
return `${counterValue - maxLetters}${afterMaxLettersCount}${maxLettersCountExceeded}`
}

// あと{count}文字
return `${beforeMaxLettersCount}${maxLetters - counterValue}${afterMaxLettersCount}`
},
[maxLetters, maxLettersCountExceeded, afterMaxLettersCount, beforeMaxLettersCount],
)

const counterVisualMessage = useMemo(() => getCounterMessage(count), [count, getCounterMessage])

useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(
ref,
() => textareaRef.current,
Expand All @@ -140,25 +184,42 @@ export const Textarea = forwardRef<HTMLTextAreaElement, Props & ElementProps>(

// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedUpdateCount = useCallback(
debounce((value) => {
debounce((value: string) => {
startTransition(() => {
setCount(getStringLength(value))
})
}, 200),
[],
)

// countが連続で更新されると、スクリーンリーダーが古い値を読み上げてしまうため、メッセージの更新を遅延しています
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedUpdateSrCounterMessage = useCallback(
debounce((value: string) => {
startTransition(() => {
const counterText = getCounterMessage(getStringLength(value))

if (counterText) {
setSrCounterMessage(counterText)
}
})
}, 1000),
[getCounterMessage],
)

const handleChange = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
if (onChange) {
onChange(event)
}

if (maxLetters) {
debouncedUpdateCount(event.currentTarget.value)
const inputValue = event.currentTarget.value
debouncedUpdateCount(inputValue)
debouncedUpdateSrCounterMessage(inputValue)
}
},
[debouncedUpdateCount, maxLetters, onChange],
[debouncedUpdateCount, maxLetters, onChange, debouncedUpdateSrCounterMessage],
)

const handleInput = useCallback(
Expand Down Expand Up @@ -198,19 +259,24 @@ export const Textarea = forwardRef<HTMLTextAreaElement, Props & ElementProps>(
style: { width: typeof width === 'number' ? `${width}px` : width },
},
counterStyle: counter(),
counterTextStyle: counterText({ error: !!(maxLetters && maxLetters - count <= 0) }),
counterTextStyle: counterText({ error: !!(maxLetters && maxLetters - count < 0) }),
}
}, [className, count, maxLetters, width])

const hasInputError = useMemo(() => {
const isCharLengthExceeded = maxLetters && count > maxLetters
return error || isCharLengthExceeded || undefined
}, [error, maxLetters, count])

const body = (
<textarea
{...props}
{...textareaStyleProps}
{...(maxLetters && { 'aria-describedby': `${maxLettersNoticeId} ${actualMaxLettersId}` })}
data-smarthr-ui-input="true"
aria-describedby={actualMaxLettersId}
onChange={handleChange}
ref={textareaRef}
aria-invalid={error || undefined}
aria-invalid={hasInputError || undefined}
rows={interimRows}
onInput={handleInput}
/>
Expand All @@ -219,13 +285,17 @@ export const Textarea = forwardRef<HTMLTextAreaElement, Props & ElementProps>(
return maxLetters ? (
<span>
{body}
<span className={counterStyle} id={actualMaxLettersId}>
{beforeMaxLettersCount}
<span className={counterTextStyle}>
{maxLetters - count}/{maxLetters}
<span className={counterStyle}>
<span className={counterTextStyle} id={actualMaxLettersId} aria-hidden={true}>
{counterVisualMessage}
</span>
{afterMaxLettersCount}
</span>
<VisuallyHiddenText aria-live="polite">{srCounterMessage}</VisuallyHiddenText>
<VisuallyHiddenText id={maxLettersNoticeId}>
{beforeScreenReaderMaxLettersDescription}
{maxLetters}
{afterScreenReaderMaxLettersDescription}
</VisuallyHiddenText>
</span>
) : (
body
Expand Down

0 comments on commit 7e62b7f

Please sign in to comment.